[0]zero-index clytie0x/zero-index

11.4 struct กับฟังก์ชันและ pointer

ในบทที่ 8 ฟังก์ชันรับค่าเป็นสำเนา และคืนค่าได้ทีละค่า ในบทที่ 10 เราส่งที่อยู่ของตัวแปรให้ฟังก์ชันแก้ค่าแทนเราได้ หน้านี้เอาสองเรื่องนั้นมาใช้กับ struct ซึ่งทำให้ได้สองอย่างที่ตัวแปรธรรมดาทำไม่ได้ คือฟังก์ชันคืนหลายค่าพร้อมกันในก้อนเดียว และเขียนฟังก์ชันเปรียบเทียบหลายเกณฑ์ที่ใช้ซ้ำได้ทั้งโปรแกรม ปิดท้ายด้วย struct ที่มี pointer ชี้หาชนิดของตัวเอง ซึ่งเป็นฐานของบทที่ 14

ส่งแบบสำเนา

ส่ง struct เข้าฟังก์ชันได้เหมือน int และได้ผลแบบเดียวกัน คือฟังก์ชันได้ สำเนาทั้งก้อน

#include <stdio.h>

typedef struct {
    char name[21];
    int score;
} Student;

void add_bonus(Student s, int bonus)
{
    s.score += bonus;
    printf("inside: %s %d\n", s.name, s.score);
}

int main()
{
    Student a = {"Mali", 87};
    add_bonus(a, 5);
    printf("outside: %s %d\n", a.name, a.score);
    return 0;
}
inside: Mali 92
outside: Mali 87

ในฟังก์ชันเห็น 92 แต่กลับมาที่ main ยังเป็น 87 เพราะ s เป็นกล่องใหม่ที่คัดลอกมาจาก a ตอนเรียก แก้ s เท่าไร a ก็ไม่รู้เรื่อง การคัดลอกนี้รวมทั้งอาเรย์ name ที่อยู่ข้างในด้วย ต่างจากการส่งอาเรย์เดี่ยวๆ ซึ่งส่งไปแค่ที่อยู่

ส่งแบบสำเนาเหมาะกับฟังก์ชันที่แค่อ่านค่า และ struct มีขนาดเล็ก เช่น Point 8 byte ถ้า struct ใหญ่ เช่นมีอาเรย์พันช่องอยู่ข้างใน ทุกครั้งที่เรียกต้องคัดลอกพันช่อง ซึ่งเปลืองเวลาโดยไม่จำเป็น

คืน struct จากฟังก์ชัน

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

#include <stdio.h>

typedef struct {
    int hours;
    int minutes;
    int seconds;
} Time;

int to_seconds(Time t)
{
    return t.hours * 3600 + t.minutes * 60 + t.seconds;
}

Time from_seconds(int total) // (1)!
{
    Time t;
    t.hours = total / 3600;
    t.minutes = total % 3600 / 60;
    t.seconds = total % 60;
    return t;
}

int main()
{
    Time start, finish;
    scanf("%d %d %d", &start.hours, &start.minutes, &start.seconds);
    scanf("%d %d %d", &finish.hours, &finish.minutes, &finish.seconds);
    Time used = from_seconds(to_seconds(finish) - to_seconds(start)); // (2)!
    printf("%d %d %d\n", used.hours, used.minutes, used.seconds);
    return 0;
}
  1. ชนิดที่คืนคือ Time ฟังก์ชันสร้าง struct ในตัวเอง ใส่ค่าครบทุกสมาชิก แล้ว return ทั้งก้อน ผู้เรียกได้สำเนาของก้อนนั้นไป
  2. รับผลด้วย = ได้ทันที เพราะ struct คัดลอกทั้งก้อนได้
9 45 30
11 20 10
1 34 40

เข้าเส้นตอน 11 ชั่วโมง 20 นาที 10 วินาที คิดเป็น 40810 วินาที ออกตัวตอน 9 ชั่วโมง 45 นาที 30 วินาที คิดเป็น 35130 วินาที ต่างกัน 5680 วินาที เท่ากับ 1 ชั่วโมง 34 นาที 40 วินาที ถ้าลบทีละหลักตรงๆ จะต้องจัดการเรื่องยืมเอง เพราะ 10 วินาทีลบ 30 วินาทีติดลบ การแปลงเป็นหน่วยเดียวก่อนตัดปัญหานี้ทิ้งทั้งหมด

ถ้าไม่มี struct ฟังก์ชัน from_seconds ต้องรับ pointer สามตัวมาเขียนผลแทน แบบในบทที่ 10 ซึ่งเรียกยากกว่าและอ่านยากกว่ามาก

ส่ง pointer และตัวดำเนินการ ->

ถ้าอยากให้ฟังก์ชันแก้ struct ของผู้เรียกจริงๆ ต้องส่งที่อยู่ไป เหมือนตัวแปรทุกชนิดในบทที่ 10 และการส่งที่อยู่ส่งแค่ขนาดของ pointer ซึ่งบนเครื่อง 64 bit คือ 8 byte ไม่ว่า struct จะใหญ่แค่ไหน

#include <stdio.h>

typedef struct {
    char name[21];
    int score;
} Student;

void read_student(Student *s)
{
    scanf("%s %d", s->name, &s->score); // (1)!
}

void add_bonus(Student *s, int bonus)
{
    (*s).score += bonus; // (2)!
    s->score += bonus; // (3)!
}

int main()
{
    Student a;
    read_student(&a);
    add_bonus(&a, 5);
    printf("%s %d\n", a.name, a.score);
    return 0;
}
  1. -> ทำก่อน & &s->score จึงเป็นที่อยู่ของช่อง score ในกล่องที่ s ชี้อยู่ ซึ่งคือที่ที่ scanf ต้องการพอดี
  2. *s คือกล่องที่ s ชี้อยู่ ซึ่งก็คือ a ใน main แล้วค่อย .score ต้องมีวงเล็บ เพราะจุดทำก่อนดอกจัน
  3. ความหมายเหมือนบรรทัดบนทุกอย่าง s->score เป็นตัวย่อของ (*s).score อ่านว่า "score ของกล่องที่ s ชี้"
Mali 87
Mali 97

บวกโบนัสสองครั้ง ครั้งละ 5 คะแนน ได้ 97 และคราวนี้ a ใน main เปลี่ยนจริง เพราะทั้งสองฟังก์ชันทำงานกับกล่องของ a โดยตรงผ่านที่อยู่

จุด หรือ ลูกศร

ดูชนิดของสิ่งที่อยู่ทางซ้าย ถ้าเป็นตัว struct เอง เช่น a หรือ students[i] ใช้ . ถ้าเป็น pointer ที่ชี้ไปหา struct เช่น s ที่ประกาศเป็น Student *s ใช้ ->

ฟังก์ชันเปรียบเทียบหลายเกณฑ์

หน้า 11.1 เห็นแล้วว่า struct ใช้ > หรือ == ตรงๆ ไม่ได้ เราต้องเขียนเองว่าก้อนไหน "ดีกว่า" และโจทย์จริงมักมีหลายเกณฑ์ เช่น คะแนนมากกว่าชนะ ถ้าคะแนนเท่ากันให้ชื่อที่มาก่อนตามลำดับตัวอักษรชนะ รวมเกณฑ์ทั้งหมดไว้ในฟังก์ชันเดียว แล้วที่อื่นในโปรแกรมแค่เรียกใช้

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[21];
    int score;
} Student;

Student students[100];

int better(const Student *a, const Student *b) // (1)!
{
    if (a->score != b->score) { // (2)!
        return a->score > b->score;
    }
    return strcmp(a->name, b->name) < 0; // (3)!
}

int find_best(const Student list[], int n) // (4)!
{
    int best = 0;
    for (int i = 1; i < n; i++) {
        if (better(&list[i], &list[best])) {
            best = i;
        }
    }
    return best;
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%s %d", students[i].name, &students[i].score);
    }
    int best = find_best(students, n);
    printf("best = %s %d\n", students[best].name, students[best].score);
    return 0;
}
  1. คืน 1 ถ้า a ดีกว่า b คืน 0 ถ้าไม่ const หน้าชนิดคือสัญญาว่าฟังก์ชันนี้จะอ่านอย่างเดียว ไม่แก้กล่องที่ชี้อยู่ ถ้าเผลอแก้ compiler จะฟ้อง ได้ทั้งความเร็วของการส่ง pointer และความปลอดภัยของการส่งสำเนา
  2. เกณฑ์แรกต่างกัน ตัดสินด้วยเกณฑ์แรกเลย a->score > b->score ได้ 1 หรือ 0 ตามบทที่ 3 พอดี
  3. มาถึงบรรทัดนี้ได้แปลว่าคะแนนเท่ากัน จึงดูเกณฑ์ถัดไป ชื่อที่ strcmp ให้ค่าติดลบคือชื่อที่มาก่อนในพจนานุกรม ถ้ามีเกณฑ์ที่สามก็ใส่ if แบบเดียวกันต่อไปเรื่อยๆ
  4. อาเรย์ที่ส่งเข้าฟังก์ชันเป็นที่อยู่ของช่องแรกเสมอ ตามบทที่ 10 ฟังก์ชันจึงแก้ข้อมูลของผู้เรียกได้ ใส่ const เพื่อบอกว่าฟังก์ชันนี้แค่หา ไม่แก้ list[i] เป็นตัว struct จึงใช้จุด และ &list[i] คือที่อยู่ของช่องนั้นที่ส่งให้ better
5
Nat 92
Mali 87
Krit 92
Ploy 78
Beam 92
best = Beam 92

มีสามคนได้ 92 เท่ากัน คือ Nat Krit และ Beam เกณฑ์ที่สองเลือก Beam เพราะ B มาก่อน K และ N ข้อดีของการแยกเป็นฟังก์ชันคือ ถ้าโจทย์เปลี่ยนเกณฑ์ แก้ที่ better ที่เดียว และฟังก์ชันหน้าตาแบบนี้จะได้ใช้อีกมากตอนเรียงข้อมูลในบทที่ 13 และบทที่ 16

ถ้าเผลอแก้ข้อมูลในฟังก์ชันที่ประกาศ const ไว้ เช่นเขียน list[i].score += 5; ในฟังก์ชันที่รับ const Student list[] จะได้

total.c:12:23: error: assignment of member 'score' in read-only object
   12 |         list[i].score += 5;
      |                       ^~

read-only แปลว่าอ่านได้อย่างเดียว นี่คือประโยชน์ของ const ความผิดพลาดที่ปกติต้องนั่งหาจากผลลัพธ์ผิดๆ กลายเป็น error ที่บอกตำแหน่งให้เลย

ขอหน่วยความจำให้อาเรย์ของ struct

ถ้าไม่อยากประกาศอาเรย์ขนาดตายตัวไว้ก่อน ใช้ malloc จากบทที่ 10 ได้เหมือนอาเรย์ชนิดอื่น สิ่งสำคัญคือขนาดต้องใช้ sizeof(Student) ห้ามบวกขนาดสมาชิกเอง เพราะ padding ในหน้า 11.3

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char name[21];
    int score;
} Student;

int main()
{
    int n;
    scanf("%d", &n);
    Student *list = malloc(n * sizeof(Student));
    if (list == NULL) {
        return 1;
    }
    for (int i = 0; i < n; i++) {
        scanf("%s %d", list[i].name, &list[i].score);
    }
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += list[i].score;
    }
    printf("%d students, total %d\n", n, total);
    printf("one student = %d bytes\n", (int)sizeof(Student));
    free(list);
    return 0;
}
3
Mali 87
Krit 92
Ploy 78
3 students, total 257
one student = 28 bytes

ชื่อ 21 byte บวกคะแนน 4 byte ได้ 25 แต่ sizeof บอก 28 เพราะ compiler เติม padding 3 byte ให้ score เริ่มที่ตำแหน่งหารด้วย 4 ลงตัว ถ้าขอ n * 25 byte เอง ช่องท้ายๆ จะเขียนเลยพื้นที่ที่ขอไว้ list[i] ใช้จุดเพราะเป็นตัว struct ส่วน list เองเป็น pointer

struct ที่ชี้หาชนิดของตัวเอง

struct มีสมาชิกเป็น struct ชนิดเดียวกับตัวเองไม่ได้ เพราะกล่องจะต้องมีกล่องแบบเดียวกันอยู่ข้างใน ซ้อนกันไม่รู้จบ แต่มี pointer ที่ชี้ไปหา struct ชนิดเดียวกันได้ เพราะ pointer มีขนาดคงที่ บนเครื่อง 64 bit ที่ใช้กันทุกวันนี้คือ 8 byte ไม่ว่าจะชี้ไปหาอะไร

#include <stdio.h>

typedef struct Node { // (1)!
    int value;
    struct Node *next; // (2)!
} Node;

int main()
{
    Node second = {20, NULL};
    Node first = {10, &second};
    printf("%d\n", first.value);
    printf("%d\n", first.next->value); // (3)!
    printf("one node = %d bytes\n", (int)sizeof(Node));
    return 0;
}
  1. กรณีนี้ต้องตั้งชื่อ struct ไว้หลังคำว่า struct ด้วย ไม่ใช่แค่ชื่อจาก typedef ท้ายปีกกา
  2. ตอนที่ compiler อ่านมาถึงบรรทัดนี้ ชื่อ Node จาก typedef ยังไม่เกิด เพราะอยู่หลังปีกกาปิด จึงต้องเรียกด้วยชื่อเต็ม struct Node
  3. first.next เป็น pointer ที่ชี้ไปหา second จึงใช้ -> หยิบ value ของกล่องที่ชี้อยู่
10
20
one node = 16 bytes

กล่องแต่ละกล่องรู้ว่ากล่องถัดไปอยู่ที่ไหน ถ้าต่อกันไปเรื่อยๆ ก็ได้ข้อมูลเป็นสายยาวแค่ไหนก็ได้ โดยเพิ่มหรือตัดกล่องตรงกลางได้โดยไม่ต้องเลื่อนข้อมูลทั้งแถวแบบอาเรย์ นี่คือ linked list ซึ่งจะสร้างจริงด้วย malloc ในบทที่ 14 พร้อมกับ stack และ queue ตอนนี้ขอให้จำแค่วิธีประกาศ struct แบบนี้ให้ได้

ลองทำให้พัง

แบบที่ 1 ใช้จุดกับ pointer

void move_right(Point *p, int steps)
{
    p.x = p.x + steps;
}
move.c:10:6: error: 'p' is a pointer; did you mean to use '->'?
   10 |     p.x = p.x + steps;
      |      ^
      |      ->
move.c:10:12: error: 'p' is a pointer; did you mean to use '->'?
   10 |     p.x = p.x + steps;
      |            ^
      |            ->

p เป็น pointer ไม่ใช่ตัว struct คราวนี้คำแนะนำของ compiler ถูก แก้เป็น p->x ถ้าเขียน *p.x โดยไม่มีวงเล็บ จะได้ error ข้อความเดียวกันนี้ เพราะจุดทำก่อนดอกจัน compiler จึงพยายามหา p.x ก่อน ต้องเขียน (*p).x หรือ p->x

แบบที่ 2 คืนที่อยู่ของ struct ที่สร้างในฟังก์ชัน

เห็นว่าส่ง pointer ประหยัดกว่า เลยลองให้ฟังก์ชันคืน pointer แทนคืนทั้งก้อน

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

Point *make_point(int x, int y)
{
    Point p = {x, y};
    return &p;
}

int main()
{
    Point *a = make_point(1, 2);
    printf("%d %d\n", a->x, a->y);
    return 0;
}
local.c:11:12: warning: function returns address of local variable [-Wreturn-local-addr]
   11 |     return &p;
      |            ^~

เป็นแค่ warning จึงรันได้ แต่ไม่พิมพ์อะไรออกมาเลย และ Code::Blocks ขึ้นว่า

Process returned -1073741819 (0xC0000005)

p เป็นตัวแปรของ make_point พอฟังก์ชันจบ กล่องของ p ก็ถูกคืนไป ที่อยู่ที่ส่งกลับมาจึงชี้ไปที่ที่ไม่มีกล่องแล้ว 0xC0000005 คือรหัสของ Windows ที่แปลว่าเข้าถึงหน่วยความจำที่ไม่มีสิทธิ์ ถ้าส่งให้ grader จะได้ runtime error วิธีแก้คือคืนทั้งก้อนแบบ from_seconds ข้างบน หรือให้ผู้เรียกส่ง pointer ของกล่องตัวเองเข้ามาให้เขียน หรือใช้ malloc ซึ่งกล่องจะอยู่จนกว่าจะ free

แบบที่ 3 ใช้ชื่อจาก typedef ก่อนที่มันจะเกิด

typedef struct {
    int value;
    Node *next;
} Node;
node.c:5:5: error: unknown type name 'Node'
    5 |     Node *next;
      |     ^~~~

ชื่อ Node เกิดตอนถึงบรรทัด } Node; ท้ายสุด บรรทัดข้างในปีกกายังไม่รู้จัก แก้ด้วยการตั้งชื่อ struct แล้วใช้ชื่อเต็ม struct Node *next; แบบในตัวอย่าง node

สรุป

  • ส่ง struct แบบสำเนา ฟังก์ชันได้กล่องใหม่ทั้งก้อน แก้แล้วผู้เรียกไม่เปลี่ยน
  • ฟังก์ชันคืน struct ได้ ใช้คืนหลายค่าพร้อมกัน
  • ส่ง pointer เพื่อให้ฟังก์ชันแก้ของจริง หรือเพื่อไม่ต้องคัดลอก struct ใหญ่ ใส่ const ถ้าแค่อ่าน
  • p->x คือ (*p).x ตัว struct ใช้จุด pointer ใช้ลูกศร
  • ฟังก์ชันเปรียบเทียบหลายเกณฑ์ ดูเกณฑ์แรกก่อน เท่ากันค่อยดูเกณฑ์ถัดไป
  • malloc(n * sizeof(Student)) ใช้ sizeof เสมอ ห้ามบวกขนาดเอง
  • ห้ามคืนที่อยู่ของตัวแปรที่สร้างในฟังก์ชัน
  • struct มี pointer ชี้ชนิดของตัวเองได้ ต้องใช้ชื่อเต็ม struct Node ข้างใน และจะใช้สร้าง linked list ในบทที่ 14

กลับไปเช็กรายการ ผ่านบทนี้เมื่อ แล้วไปทำแบบฝึกหัด