[0]zero-index clytie0x/zero-index

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;
}
  1. รับที่อยู่ของ int สองตัว ไม่ใช่ค่า int สองตัว
  2. *x คือกล่องที่ x ชี้ ตอนนี้คือ first ของ main สามบรรทัดนี้คือการสลับแบบใช้ตัวพักเหมือนหน้า 8.2 ทุกอย่าง แค่ทุกชื่อมี * นำหน้า
  3. ส่งที่อยู่ของ 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 ที่เกิดใหม่ตอนเรียก

main swap first 3 → 8 …FB0C second 8 → 3 …FB08 x …FB0C …FAE0 y …FB08 …FAE8

เทียบกับโปรแกรม 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;
}
  1. a เป็นที่อยู่อยู่แล้ว ส่งต่อให้ swap ได้เลย ไม่ต้องมี & ถ้าเขียน &a จะได้ที่อยู่ของกล่อง a เอง ซึ่งมีชนิดเป็น int ** (อธิบายในหน้า 10.5) ไม่ใช่ที่อยู่ของความสูง
  2. &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;
}
  1. พารามิเตอร์ตัวแรกเป็นข้อมูลเข้า รับเป็นค่าธรรมดา สองตัวหลังเป็นที่สำหรับเขียนผล รับเป็น pointer ตั้งชื่อให้บอกว่าจะได้อะไรกลับไป
  2. ค่าเดิมใน 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. เจอแล้ว เขียนที่นั่งลงกล่องของคนเรียก แล้วคืน 1 ว่าสำเร็จ
  2. ไม่เจอ คืน 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;
}
  1. 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 ต้องมี & เพราะเป็นฟังก์ชันที่ต้องเขียนลงตัวแปรของเรา ลืมแล้วมันจะเอาค่าขยะไปใช้เป็นที่อยู่
  • ห้ามคืนที่อยู่ของตัวแปรในฟังก์ชัน กล่องนั้นหายไปตอนฟังก์ชันจบ