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 (ส่งค่าแบบสำเนา)
ถ้ายังไม่เชื่อว่าต้องเป็นสำเนา ลองนึกถึง 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;
}
- ส่งค่า 70 เข้าไป ได้ 75 กลับมา แล้วเก็บทับ
scoreเอง เหมือนscore = score + 5;ในบทที่ 1 - เพราะฟังก์ชันไม่แก้ตัวแปรของใคร จึงใช้กับค่าใดก็ได้ ไม่จำเป็นต้องเป็นตัวแปร
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 ไม่เตือน ผลหายไปเฉยๆ