[0]zero-index clytie0x/zero-index

8.2 ส่งค่าแบบสำเนา

หน้าที่แล้วส่งค่าเข้าฟังก์ชันแล้วใช้อ่านอย่างเดียว คำถามต่อมาที่น้องเกือบทุกคนลองคือ ถ้าฟังก์ชันแก้ค่าพารามิเตอร์ ตัวแปรที่ส่งมาจาก main จะเปลี่ยนตามไหม คำตอบคือไม่เปลี่ยน หน้านี้จะทำให้เห็นกับตาว่าทำไม แล้วดูวิธีที่ถูกต้องในการเอาผลออกมาจากฟังก์ชัน และเรื่องชนิดของค่าที่ถูกแปลงเงียบๆ ตอนส่งเข้าและส่งออก

พารามิเตอร์คือสำเนา

ครูให้คะแนนพิเศษคนละ 5 คะแนน ลองเขียนฟังก์ชันบวกคะแนนให้

#include <stdio.h>

void add_bonus(int score)
{
    score = score + 5;
    printf("inside add_bonus: %d\n", score);
}

int main()
{
    int score = 70;
    add_bonus(score);
    printf("in main: %d\n", score);
    return 0;
}
inside add_bonus: 75
in main: 70

ในฟังก์ชันได้ 75 จริง แต่กลับมาที่ main ยังเป็น 70 เหมือนเดิม ทั้งที่ชื่อ score เหมือนกันทุกตัวอักษร

เหตุผลคือตอนเรียก add_bonus(score) ภาษา C ไม่ได้ส่งกล่อง score ของ main ไปให้ แต่ คัดลอกค่า 70 ไปใส่กล่องใหม่ที่เป็นของ add_bonus เอง กล่องใหม่นี้บังเอิญชื่อ score เหมือนกัน แต่เป็นคนละกล่อง แก้กล่องไหนก็ไม่กระทบอีกกล่อง พอฟังก์ชันจบ กล่องของ add_bonus ก็หายไปพร้อมค่า 75 วิธีส่งแบบนี้เรียกว่า pass by value (ส่งค่าแบบสำเนา)

main score 70 add_bonus score 70 → 75 copy 70 two boxes, same name

ถ้ายังไม่เชื่อว่าต้องเป็นสำเนา ลองนึกถึง add_bonus(70) หรือ add_bonus(score + 10) ซึ่งเรียกได้ทั้งคู่ เลข 70 ไม่ใช่กล่อง จะไปแก้ค่าของมันไม่ได้อยู่แล้ว สิ่งที่ฟังก์ชันได้รับจึงเป็นแค่ ค่า ไม่ใช่ตัวแปร

สลับค่าในฟังก์ชันไม่ได้

ตัวอย่างที่ทำให้หลายคนงงคือฟังก์ชันสลับค่าสองตัว โค้ดสลับในฟังก์ชันถูกทุกบรรทัด แต่ผลออกมาไม่สลับ

#include <stdio.h>

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
    printf("inside swap: a = %d, b = %d\n", a, b);
}

int main()
{
    int first = 3, second = 8;
    swap(first, second);
    printf("in main: first = %d, second = %d\n", first, second);
    return 0;
}
inside swap: a = 8, b = 3
in main: first = 3, second = 8

a กับ b เป็นสำเนาของ 3 และ 8 สลับกันเรียบร้อยในฟังก์ชัน แล้วก็หายไปตอนฟังก์ชันจบ first กับ second ไม่เคยถูกแตะเลย ฟังก์ชันที่แก้ตัวแปรของคนเรียกได้จริงต้องส่ง ที่อยู่ ของกล่องเข้าไปแทนค่า ซึ่งต้องใช้ pointer ในบทที่ 10

นี่คือเหตุผลเดียวกับที่ scanf ต้องมี & นำหน้าตัวแปรตั้งแต่บทที่ 1 scanf เป็นฟังก์ชัน ถ้าส่ง age ไปเฉยๆ มันจะได้แค่สำเนาของค่าขยะในกล่อง ใส่ค่าที่อ่านมาลงไปก็ไม่ถึงตัวแปรของเรา &age คือการส่งที่อยู่ของกล่องไปแทน scanf จึงเขียนลงกล่องจริงได้

อยากได้ผล ให้ส่งกลับมา

ตอนนี้ยังไม่มี pointer วิธีที่ถูกคือให้ฟังก์ชัน คืนค่าใหม่ แล้วคนเรียกเป็นคนตัดสินใจเองว่าจะเก็บไว้ที่ไหน

#include <stdio.h>

int add_bonus(int score)
{
    return score + 5;
}

int main()
{
    int score = 70;
    score = add_bonus(score); // (1)!
    printf("in main: %d\n", score);
    int other = add_bonus(88); // (2)!
    printf("other: %d\n", other);
    return 0;
}
  1. ส่งค่า 70 เข้าไป ได้ 75 กลับมา แล้วเก็บทับ score เอง เหมือน score = score + 5; ในบทที่ 1
  2. เพราะฟังก์ชันไม่แก้ตัวแปรของใคร จึงใช้กับค่าใดก็ได้ ไม่จำเป็นต้องเป็นตัวแปร
in main: 75
other: 93

ฟังก์ชันที่รับค่า คำนวณ แล้วส่งค่ากลับ โดยไม่ไปยุ่งกับอะไรข้างนอก เข้าใจง่ายที่สุดและทดสอบง่ายที่สุด เพราะส่งค่าเดิมเข้าไปกี่ครั้งก็ได้คำตอบเดิม ฟังก์ชันส่วนใหญ่ในโจทย์แข่งขันควรเป็นแบบนี้

return ได้ค่าเดียว

return ส่งกลับได้ครั้งละค่าเดียว ถ้าต้องการสองผล เช่น แปลงนาทีเป็นชั่วโมงกับนาที วิธีที่ทำได้ตอนนี้คือแยกเป็นสองฟังก์ชัน แต่ละตัวตอบหนึ่งคำถาม

#include <stdio.h>

int hours_of(int total_minutes)
{
    return total_minutes / 60;
}

int minutes_of(int total_minutes)
{
    return total_minutes % 60;
}

int main()
{
    int movie, trip;
    scanf("%d %d", &movie, &trip);
    printf("movie %d:%02d\n", hours_of(movie), minutes_of(movie));
    printf("trip %d:%02d\n", hours_of(trip), minutes_of(trip));
    return 0;
}
135 425
movie 2:15
trip 7:05

ถ้าแค่ต้องพิมพ์ ก็เขียนเป็นฟังก์ชัน void ที่พิมพ์ทั้งสองค่าเองได้อีกทาง ส่วนวิธีที่ให้ฟังก์ชันเดียวส่งผลกลับมาหลายค่าจริงๆ มีสองวิธี คือส่งที่อยู่ของตัวแปรเข้าไปด้วย pointer ในบทที่ 10 และรวมหลายค่าเป็นก้อนเดียวด้วย struct ในบทที่ 11

ชนิดของค่าถูกแปลงตอนส่ง

ค่าที่ส่งเข้าไปจะถูกแปลงเป็นชนิดของพารามิเตอร์ และค่าที่ return จะถูกแปลงเป็นชนิดที่เขียนไว้หน้าชื่อฟังก์ชัน เหมือนการเอาค่าใส่ตัวแปรในหน้า 1.4 ทุกอย่าง ถ้าแปลงจาก double เป็น int ทศนิยมจะถูกตัดทิ้งเงียบๆ

#include <stdio.h>

int square_int(int side)
{
    return side * side;
}

double square(double side)
{
    return side * side;
}

int main()
{
    double side;
    scanf("%lf", &side);
    printf("%d\n", square_int(side));
    printf("%f\n", square(side));
    printf("%f\n", square(3));
    return 0;
}
2.5
4
6.250000
9.000000
  • บรรทัดที่ 17 ส่ง 2.5 เข้าพารามิเตอร์ int ถูกตัดเหลือ 2 ตั้งแต่ตอนเข้า จึงได้ 4 แทนที่จะเป็น 6.25 และ -Wall ไม่เตือน
  • บรรทัดที่ 19 ส่ง 3 ที่เป็น int เข้าพารามิเตอร์ double ได้ 3.0 ไม่มีอะไรหาย การแปลงทางนี้ปลอดภัย

กฎจึงเหมือนตอนประกาศตัวแปร ค่าไหนมีทศนิยมได้ พารามิเตอร์และชนิดที่ส่งกลับต้องเป็น double ค่าไหนเกิน 2 × 109 ได้ ต้องเป็น long long ทั้งพารามิเตอร์ ชนิดที่ส่งกลับ และตัวแปรที่รับผล ถ้าลืมจุดใดจุดหนึ่งก็ล้นหรือหายตรงจุดนั้น

ลองทำให้พัง

แบบที่ 1 อ่านค่าเข้าพารามิเตอร์

อยากแยกส่วนรับข้อมูลออกเป็นฟังก์ชัน เลยเขียนแบบนี้

#include <stdio.h>

void read_score(int score)
{
    scanf("%d", &score);
}

int main()
{
    int score;
    read_score(score);
    printf("score = %d\n", score);
    return 0;
}
reader.c:11:5: warning: 'score' is used uninitialized [-Wuninitialized]
   11 |     read_score(score);
      |     ^~~~~~~~~~~~~~~~~
reader.c:10:9: note: 'score' was declared here
   10 |     int score;
      |         ^~~~~

scanf เขียนค่าลงกล่อง score ของ read_score ซึ่งเป็นสำเนา ส่วนกล่องของ main ไม่เคยได้ค่าเลย compiler จึงเตือนว่าใช้ตัวแปรที่ยังไม่ได้ใส่ค่า บนเครื่องที่เขียนเอกสารนี้ ป้อน 85 แล้วได้ score = 0 แก้ให้ฟังก์ชันอ่านค่าใส่ตัวแปรของตัวเอง แล้ว return ค่านั้นกลับมา

#include <stdio.h>

int read_score()
{
    int score;
    scanf("%d", &score);
    return score;
}

int main()
{
    int score = read_score();
    printf("score = %d\n", score);
    return 0;
}
85
score = 85

แบบที่ 2 เรียกแล้วไม่เก็บค่าที่คืน

ในโปรแกรม bonus-return แก้บรรทัดที่ 11 เหลือแค่ add_bonus(score);

#include <stdio.h>

int add_bonus(int score)
{
    return score + 5;
}

int main()
{
    int score = 70;
    add_bonus(score);
    printf("in main: %d\n", score);
    return 0;
}
in main: 70

คอมไพล์ผ่านโดยไม่มีคำเตือนใดๆ ฟังก์ชันคำนวณ 75 ให้แล้ว แต่ไม่มีใครรับไว้ ค่าเลยถูกทิ้งไป ภาษา C ยอมให้เรียกฟังก์ชันแล้วไม่สนค่าที่คืน เพราะบางฟังก์ชันอย่าง printf ก็คืนค่าที่คนส่วนใหญ่ไม่ใช้ ถ้าเจอฟังก์ชันที่คืนค่าแต่ถูกเรียกเป็นคำสั่งเดี่ยวๆ ให้สงสัยไว้ก่อนว่าลืมเก็บผล

แบบที่ 3 ชนิดที่ส่งกลับไม่พอ

ฟังก์ชันหาค่าเฉลี่ยที่แปลงเป็น double ก่อนหารอย่างถูกต้องตามหน้า 1.4 แต่ประกาศให้ส่ง int กลับ

#include <stdio.h>

int average(int total, int count)
{
    return (double)total / count;
}

int main()
{
    int total, count;
    scanf("%d %d", &total, &count);
    printf("%f\n", (double)average(total, count));
    return 0;
}
245 3
81.000000

การหารในฟังก์ชันได้ 81.666667 ถูกแล้ว แต่ตอน return ค่าถูกแปลงเป็น int ตามหัวฟังก์ชัน เหลือ 81 พอถึง main จะแปลงกลับเป็น double ก็สายไปแล้ว เหมือนกับดักในหน้า 1.4 ที่หารเสร็จแล้วค่อยเก็บใน double แก้ที่หัวฟังก์ชันเป็น double average(int total, int count) และพิมพ์ด้วย %f ตรงๆ ได้เลย

สรุป

  • อาร์กิวเมนต์ถูกคัดลอกค่าไปใส่พารามิเตอร์ ฟังก์ชันแก้พารามิเตอร์แล้วตัวแปรของคนเรียกไม่เปลี่ยน แม้ชื่อจะเหมือนกัน
  • อยากได้ผลจากฟังก์ชัน ให้ return ค่าใหม่ แล้วคนเรียกเก็บเอง เช่น score = add_bonus(score);
  • return ส่งได้ค่าเดียว ส่งหลายค่าต้องรอ pointer ในบทที่ 10 หรือ struct ในบทที่ 11
  • ค่าที่ส่งเข้าและส่งออกถูกแปลงชนิดเงียบๆ เลือกชนิดของพารามิเตอร์และชนิดที่ส่งกลับให้ใหญ่พอเสมอ
  • เรียกฟังก์ชันที่คืนค่าแล้วไม่เก็บผล compiler ไม่เตือน ผลหายไปเฉยๆ