10.2 ส่งที่อยู่ให้ฟังก์ชัน
ในหน้า 8.2 ฟังก์ชัน swap สลับค่าได้แค่ในตัวเอง เพราะได้รับแค่สำเนา และ return ส่งผลกลับได้ทีละค่า หน้านี้แก้ทั้งสองเรื่องด้วยสิ่งที่เรียนในหน้า 10.1 คือส่ง ที่อยู่ ของตัวแปรเข้าไปแทนค่า ฟังก์ชันจะตามที่อยู่นั้นไปแก้กล่องของคนเรียกได้ เทคนิคนี้ใช้ทุกครั้งที่ฟังก์ชันต้องแก้ตัวแปรข้างนอก หรือต้องส่งผลกลับหลายค่า และเป็นเหตุผลจริงที่ scanf ต้องมี & มาตั้งแต่บทที่ 1
swap ที่สลับได้จริง
เปลี่ยนพารามิเตอร์จาก int เป็น int * แล้วสลับค่าในกล่องที่ชี้ แทนการสลับตัวพารามิเตอร์เอง
#include <stdio.h>
void swap(int *x, int *y) // (1)!
{
int temp = *x; // (2)!
*x = *y;
*y = temp;
}
int main()
{
int first = 3, second = 8;
swap(&first, &second); // (3)!
printf("first = %d, second = %d\n", first, second);
return 0;
}
- รับที่อยู่ของ
intสองตัว ไม่ใช่ค่าintสองตัว *xคือกล่องที่xชี้ ตอนนี้คือfirstของmainสามบรรทัดนี้คือการสลับแบบใช้ตัวพักเหมือนหน้า 8.2 ทุกอย่าง แค่ทุกชื่อมี*นำหน้า- ส่งที่อยู่ของ
firstกับsecondเข้าไป ตัวที่ถูกคัดลอกเข้าฟังก์ชันคือที่อยู่ ไม่ใช่ 3 กับ 8
first = 8, second = 3
ภาษา C ยังส่งแบบสำเนาเหมือนเดิมทุกอย่าง ไม่มีกฎใหม่ แต่ของที่ถูกคัดลอกคือที่อยู่ สำเนาของที่อยู่ก็ยังชี้กล่องเดิม เหมือนถ่ายเอกสารแผนที่บ้านให้เพื่อน แผนที่มีสองใบ แต่บ้านมีหลังเดียว เพื่อนเดินตามแผนที่ของตัวเองไปทาสีบ้าน บ้านเราก็เปลี่ยนสี
ดูที่อยู่ตอนเรียก swap
เพิ่ม printf ที่พิมพ์ที่อยู่ลงไปในโปรแกรมเดิม จะเห็นว่าในฟังก์ชันมีอะไรอยู่จริงๆ
#include <stdio.h>
void swap(int *x, int *y)
{
printf("x = %p, y = %p\n", x, y);
printf("&x = %p, &y = %p\n", &x, &y);
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int first = 3, second = 8;
printf("&first = %p, &second = %p\n", &first, &second);
swap(&first, &second);
printf("first = %d, second = %d\n", first, second);
return 0;
}
&first = 000000EF7FFFFB0C, &second = 000000EF7FFFFB08
x = 000000EF7FFFFB0C, y = 000000EF7FFFFB08
&x = 000000EF7FFFFAE0, &y = 000000EF7FFFFAE8
first = 8, second = 3
ผลนี้มาจากเครื่องที่เขียนเอกสารนี้ ที่อยู่จะเปลี่ยนทุกครั้งที่รันตามที่เห็นในหน้า 10.1 แต่ความสัมพันธ์เหมือนเดิมเสมอ ค่าใน x ตรงกับที่อยู่ของ first ค่าใน y ตรงกับที่อยู่ของ second ส่วนกล่อง x กับ y เองอยู่คนละที่กับ first และ second เพราะเป็นพารามิเตอร์ของ swap ที่เกิดใหม่ตอนเรียก
เทียบกับโปรแกรม swap ในหน้า 8.2 ที่ swap มีกล่อง a กับ b ที่เก็บ 3 กับ 8 เป็นของตัวเอง ตอนนี้กล่องของ swap เก็บแค่ทาง ส่วนตัวเลขที่ถูกสลับคือของ main พอ swap จบ กล่อง x กับ y หายไปเหมือนเดิม แต่การแก้ที่ทำผ่านลูกศรยังอยู่
ส่ง pointer ต่อให้ฟังก์ชันอื่น
ที่อยู่ของช่องในอาเรย์ เช่น &heights[0] ก็เป็นที่อยู่ของกล่อง int หนึ่งกล่อง ส่งให้ swap ได้เหมือนตัวแปรธรรมดา ตัวอย่างนี้เรียงความสูงสามคนจากน้อยไปมาก
#include <stdio.h>
void swap(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
}
void sort3(int *a, int *b, int *c)
{
if (*a > *b) {
swap(a, b); // (1)!
}
if (*b > *c) {
swap(b, c);
}
if (*a > *b) {
swap(a, b);
}
}
int main()
{
int heights[3];
scanf("%d %d %d", &heights[0], &heights[1], &heights[2]);
sort3(&heights[0], &heights[1], &heights[2]); // (2)!
printf("%d %d %d\n", heights[0], heights[1], heights[2]);
return 0;
}
aเป็นที่อยู่อยู่แล้ว ส่งต่อให้swapได้เลย ไม่ต้องมี&ถ้าเขียน&aจะได้ที่อยู่ของกล่องaเอง ซึ่งมีชนิดเป็นint **(อธิบายในหน้า 10.5) ไม่ใช่ที่อยู่ของความสูง&heights[0]ใช้แบบเดียวกับในscanfบรรทัดบน
172 158 165
158 165 172
หลังเทียบคู่แรกกับคู่ที่สอง ค่ามากสุดไปอยู่ท้ายแน่นอนแล้ว เทียบคู่แรกอีกครั้งก็เรียงครบ วิธีคิดนี้คือ bubble sort แบบย่อส่วน ซึ่งจะได้เรียนเต็มๆ ในบทที่ 13
กฎจำง่ายคือ ดูชนิดของสิ่งที่มีอยู่ในมือ ถ้ามีกล่อง int และฟังก์ชันอยากได้ int * ให้ใส่ & ถ้ามี int * อยู่แล้ว ส่งไปตรงๆ
ส่งผลกลับหลายค่า
หน้า 8.2 ต้องแยกฟังก์ชันแปลงนาทีเป็นสองตัว hours_of กับ minutes_of เพราะ return ได้ค่าเดียว ตอนนี้ให้คนเรียกส่งที่อยู่ของกล่องรับผลมา แล้วฟังก์ชันเขียนผลลงไปเองได้ทั้งสองค่า
#include <stdio.h>
void to_hours(int total_minutes, int *hours, int *minutes) // (1)!
{
*hours = total_minutes / 60;
*minutes = total_minutes % 60;
}
int main()
{
int movie, trip;
scanf("%d %d", &movie, &trip);
int h, m;
to_hours(movie, &h, &m); // (2)!
printf("movie %d:%02d\n", h, m);
to_hours(trip, &h, &m);
printf("trip %d:%02d\n", h, m);
return 0;
}
- พารามิเตอร์ตัวแรกเป็นข้อมูลเข้า รับเป็นค่าธรรมดา สองตัวหลังเป็นที่สำหรับเขียนผล รับเป็น pointer ตั้งชื่อให้บอกว่าจะได้อะไรกลับไป
- ค่าเดิมใน
hกับmไม่สำคัญ เพราะฟังก์ชันจะเขียนทับทั้งคู่
135 425
movie 2:15
trip 7:05
ได้ผลเท่ากับในหน้า 8.2 แต่คิดในฟังก์ชันเดียว ถ้าการคำนวณสองค่าใช้ขั้นตอนร่วมกัน เช่น แยกหลักของตัวเลขแล้วได้ทั้งจำนวนหลักและผลรวมของหลัก วิธีนี้ทำให้ไม่ต้องวนแยกหลักซ้ำสองรอบ
return บอกว่าสำเร็จไหม pointer ส่งคำตอบ
รูปแบบที่เจอบ่อยมากคือใช้ return บอกว่าทำสำเร็จหรือไม่ แล้วส่งคำตอบจริงกลับทาง pointer ตัวอย่างนี้หาว่านักเรียนเลขประจำตัวที่ถามนั่งที่นั่งไหน ถ้าไม่มาสอบก็ไม่มีที่นั่งให้ตอบ
#include <stdio.h>
int find_seat(int ids[], int n, int target, int *seat)
{
for (int i = 0; i < n; i++) {
if (ids[i] == target) {
*seat = i + 1;
return 1; // (1)!
}
}
return 0; // (2)!
}
int main()
{
int ids[100];
int n, q;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &ids[i]);
}
scanf("%d", &q);
for (int k = 0; k < q; k++) {
int target, seat;
scanf("%d", &target);
if (find_seat(ids, n, target, &seat)) {
printf("%d sits at seat %d\n", target, seat);
} else {
printf("%d is absent\n", target);
}
}
return 0;
}
- เจอแล้ว เขียนที่นั่งลงกล่องของคนเรียก แล้วคืน 1 ว่าสำเร็จ
- ไม่เจอ คืน 0 และไม่แตะ
*seatเลย คนเรียกจึงต้องดูค่าที่คืนก่อนทุกครั้ง ห้ามใช้seatตอนได้ 0
5
6701 6705 6712 6703 6720
3
6712
6704
6701
6712 sits at seat 3
6704 is absent
6701 sits at seat 1
คำตอบ "ไม่เจอ" แยกจากคำตอบปกติชัดเจน ไม่ต้องตกลงกันว่าที่นั่ง 0 หรือ -1 แปลว่าไม่มา ซึ่งใช้ไม่ได้ถ้าค่าเหล่านั้นเป็นคำตอบจริงได้
ทำไม scanf ต้องมี &
ถึงตรงนี้ก็ตอบคำถามที่ค้างมาตั้งแต่หน้า 1.1 ได้ครบ scanf ก็เป็นฟังก์ชันแบบที่เราเขียนเอง มันได้รับสำเนาของทุกอย่างที่ส่งเข้าไป ถ้าส่งค่าของ age ไป มันได้แค่ตัวเลขในกล่อง ไม่รู้ว่ากล่องอยู่ไหน จึงเขียนค่าที่อ่านได้กลับมาไม่ได้ ต้องส่งที่อยู่ &age ไป scanf ถึงตามไปเขียนได้ เหมือน to_hours ที่ต้องได้ &h ส่วน printf แค่เอาค่าไปพิมพ์ ไม่ต้องแก้อะไร จึงรับค่าธรรมดา
ค่าที่ scanf คืนคือจำนวนช่องที่อ่านสำเร็จ ซึ่งใช้มาแล้วใน while (scanf("%d", &x) == 1) ของบทที่ 4 ก็คือรูปแบบเดียวกับ find_seat ข้างบนเป๊ะ คืนค่าบอกว่าสำเร็จแค่ไหน แล้วเขียนผลจริงผ่าน pointer
ฟังก์ชันอ่านค่าที่พังในส่วนลองทำให้พังของหน้า 8.2 ก็แก้ได้ด้วยวิธีเดียวกัน คือรับที่อยู่ แล้วส่งที่อยู่นั้นต่อให้ scanf ตรงๆ
#include <stdio.h>
void read_score(int *score)
{
scanf("%d", score); // (1)!
}
int main()
{
int math, science;
read_score(&math);
read_score(&science);
printf("total = %d\n", math + science);
return 0;
}
scoreเป็นที่อยู่อยู่แล้ว จึงไม่มี&ตามกฎในหัวข้อที่แล้ว
78 91
total = 169
ลืม & แล้วเกิดอะไรจริงๆ
หน้า 1.1 ลอง scanf("%d", age); แล้วได้ next year you will be 1 ตอนนี้อธิบายได้แล้ว scanf ได้ค่าใน age ไปและถือว่าเป็นที่อยู่ บนเครื่องที่เขียนเอกสารนี้ค่าขยะใน age คือ 0 ซึ่งก็คือ NULL ที่อยู่ที่ไม่ชี้อะไร ลองเก็บค่าที่ scanf คืนมาพิมพ์ดู บนเครื่องนี้ได้ 0 คือ scanf ไม่ได้เขียนอะไรลงไปเลย age จึงยังเป็น 0 แล้วบวก 1 ได้ 1
ถ้าค่าขยะเป็นเลขอื่นจะร้ายกว่านั้น ลองให้ age มีค่าก่อนอ่าน
#include <stdio.h>
int main()
{
int age = 12345;
int got = scanf("%d", age);
printf("scanf returned %d, age = %d\n", got, age);
return 0;
}
ได้คำเตือน format '%d' expects argument of type 'int *' เหมือนในหน้า 1.1 พอรันแล้วพิมพ์ 20 โปรแกรมปิดตัวทันทีโดยไม่พิมพ์อะไร บนเครื่องที่เขียนเอกสารนี้ได้
Process returned -1073741819 (0xC0000005)
scanf พยายามเขียน 20 ลงที่อยู่หมายเลข 12345 ซึ่งไม่ใช่หน่วยความจำของโปรแกรมเรา Windows จึงปิดโปรแกรม ค่าขยะจริงเป็นเลขอะไรก็ได้ บางเครื่องอาจบังเอิญเป็นที่อยู่ของตัวแปรอื่น แล้วตัวแปรนั้นเปลี่ยนค่าเงียบๆ คำเตือนเรื่อง format ใน scanf จึงต้องแก้ทุกครั้ง
ลองทำให้พัง
แบบที่ 1 ลืม & ตอนเรียก
ในโปรแกรม swap แก้บรรทัดที่ 13 เป็น swap(first, second);
swap.c:13:10: error: passing argument 1 of 'swap' makes pointer from integer without a cast [-Wint-conversion]
13 | swap(first, second);
| ^~~~~
| |
| int
swap.c:3:16: note: expected 'int *' but argument is of type 'int'
3 | void swap(int *x, int *y)
| ~~~~~^
อาร์กิวเมนต์ตัวที่สองได้ error แบบเดียวกันซ้ำอีกชุด ข้อความบอกตรงตัวว่าฟังก์ชันรอ int * แต่ได้ int และ note ชี้กลับไปที่หัวฟังก์ชันให้ดูว่าประกาศไว้ว่าอะไร GCC 14 ถือเป็น error ทันที จึงลืม & ตอนเรียกฟังก์ชันของเราเองไม่ได้ ต่างจาก scanf ที่ได้แค่ warning เพราะหัวฟังก์ชันของ scanf ไม่ได้บอกชนิดของอาร์กิวเมนต์ไว้ compiler ตรวจได้แค่จากรูปแบบอย่าง %d ในข้อความ
แบบที่ 2 สลับที่อยู่แทนสลับค่า
ลืมดอกจันในฟังก์ชัน แล้วแก้ชนิดของ temp จนคอมไพล์ผ่าน
#include <stdio.h>
void swap(int *x, int *y)
{
int *temp = x;
x = y;
y = temp;
}
int main()
{
int first = 3, second = 8;
swap(&first, &second);
printf("first = %d, second = %d\n", first, second);
return 0;
}
first = 3, second = 8
ไม่มีคำเตือนเลย และไม่สลับ ฟังก์ชันนี้สลับค่าใน x กับ y ซึ่งเป็นสำเนาของที่อยู่ ให้ลูกศรสองเส้นของ swap ไขว้กัน แต่ไม่เคยตามลูกศรไปแตะกล่องของ main เลย พอฟังก์ชันจบลูกศรก็หายไป ถ้าตั้งใจแก้ค่าในกล่องของคนเรียก ทุกบรรทัดที่เขียนต้องมี * หน้าชื่อ pointer
แบบที่ 3 สลับด้วยบวกลบ แล้วชี้กล่องเดียวกัน
มีเทคนิคสลับค่าโดยไม่ใช้ตัวพัก ด้วยการบวกลบสามบรรทัด ดูฉลาดดี แต่ถ้า x กับ y ชี้กล่องเดียวกัน ซึ่งเกิดเมื่อสั่งสลับช่องกับตัวเอง ค่าในกล่องจะหายไป
#include <stdio.h>
void swap(int *x, int *y)
{
*x = *x + *y;
*y = *x - *y;
*x = *x - *y;
}
int main()
{
int seats[3] = {31, 27, 45};
swap(&seats[0], &seats[2]);
printf("%d %d %d\n", seats[0], seats[1], seats[2]);
swap(&seats[1], &seats[1]);
printf("%d %d %d\n", seats[0], seats[1], seats[2]);
return 0;
}
45 27 31
45 0 31
สลับคนละช่องถูก แต่สลับช่องที่ 1 กับตัวเอง 27 กลายเป็น 0 ไล่ทีละบรรทัดเมื่อ *x กับ *y เป็นกล่องเดียวกัน บรรทัดที่ 5 ได้ 27 + 27 = 54 บรรทัดที่ 6 ได้ 54 - 54 = 0 ซึ่งเขียนทับกล่องเดียวกันนั้น บรรทัดที่ 7 ได้ 0 - 0 = 0 โจทย์ที่ให้สลับตามคำสั่ง มักมีคำสั่งที่ให้สลับกับตัวเองซ่อนอยู่ในชุดทดสอบ ใช้ตัวพักแบบตัวอย่างแรกของหน้านี้ ซึ่งถูกเสมอและอ่านง่ายกว่า
แบบที่ 4 คืนที่อยู่ของตัวแปรในฟังก์ชัน
ถ้าคืนผลทาง pointer ได้ บางคนจะลองคืนที่อยู่ของตัวแปรในฟังก์ชันแทนการคืนค่า
#include <stdio.h>
int *bigger(int a, int b)
{
int result = a;
if (b > a) {
result = b;
}
return &result;
}
int main()
{
int *p = bigger(70, 85);
printf("bigger = %d\n", *p);
return 0;
}
bigger.c:9:12: warning: function returns address of local variable [-Wreturn-local-addr]
9 | return &result;
| ^~~~~~~
เป็นแค่ warning จึงรันได้ แต่ไม่พิมพ์อะไรเลย และ Code::Blocks ขึ้นว่า
Process returned -1073741819 (0xC0000005)
result เป็นตัวแปรของ bigger พอฟังก์ชันจบกล่องก็หายไปตามที่เห็นในหน้า 8.4 ที่อยู่ของกล่องที่ไม่มีแล้วจึงใช้ไม่ได้ GCC รู้ว่าเป็นบั๊กแน่ๆ จึงคืน NULL แทนที่อยู่นั้นให้เลย ลองพิมพ์ p == NULL บนเครื่องที่เขียนเอกสารนี้ได้ 1 โปรแกรมจึงพังทันทีที่ใช้ *p แบบที่ 5 ของหน้า 10.1 ฟังก์ชันแบบนี้ให้คืนค่า int ธรรมดา หรือให้คนเรียกส่งที่อยู่ของกล่องตัวเองมาให้เขียนแบบ to_hours ส่วนการสร้างกล่องที่อยู่ได้นานกว่าฟังก์ชันจะเรียนในหน้า 10.5
สรุป
- ฟังก์ชันที่ต้องแก้ตัวแปรของคนเรียก ให้รับ pointer เช่น
void swap(int *x, int *y)แล้วเรียกด้วยswap(&a, &b) - ในฟังก์ชัน แก้ผ่าน
*xเสมอ ถ้าเขียนแค่xคือแก้สำเนาของที่อยู่ ไม่ถึงกล่องจริง - มี
intแต่ต้องส่งint *ใส่&มีint *อยู่แล้ว ส่งตรงๆ - ส่งผลกลับหลายค่าด้วยพารามิเตอร์ pointer หลายตัว และใช้
returnบอกว่าสำเร็จไหมได้แบบเดียวกับscanf scanfต้องมี&เพราะเป็นฟังก์ชันที่ต้องเขียนลงตัวแปรของเรา ลืมแล้วมันจะเอาค่าขยะไปใช้เป็นที่อยู่- ห้ามคืนที่อยู่ของตัวแปรในฟังก์ชัน กล่องนั้นหายไปตอนฟังก์ชันจบ