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;
}
- ชนิดที่คืนคือ
Timeฟังก์ชันสร้าง struct ในตัวเอง ใส่ค่าครบทุกสมาชิก แล้วreturnทั้งก้อน ผู้เรียกได้สำเนาของก้อนนั้นไป - รับผลด้วย
=ได้ทันที เพราะ 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;
}
->ทำก่อน&&s->scoreจึงเป็นที่อยู่ของช่อง score ในกล่องที่sชี้อยู่ ซึ่งคือที่ที่scanfต้องการพอดี*sคือกล่องที่sชี้อยู่ ซึ่งก็คือaในmainแล้วค่อย.scoreต้องมีวงเล็บ เพราะจุดทำก่อนดอกจัน- ความหมายเหมือนบรรทัดบนทุกอย่าง
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 ถ้า
aดีกว่าbคืน 0 ถ้าไม่constหน้าชนิดคือสัญญาว่าฟังก์ชันนี้จะอ่านอย่างเดียว ไม่แก้กล่องที่ชี้อยู่ ถ้าเผลอแก้ compiler จะฟ้อง ได้ทั้งความเร็วของการส่ง pointer และความปลอดภัยของการส่งสำเนา - เกณฑ์แรกต่างกัน ตัดสินด้วยเกณฑ์แรกเลย
a->score > b->scoreได้ 1 หรือ 0 ตามบทที่ 3 พอดี - มาถึงบรรทัดนี้ได้แปลว่าคะแนนเท่ากัน จึงดูเกณฑ์ถัดไป ชื่อที่
strcmpให้ค่าติดลบคือชื่อที่มาก่อนในพจนานุกรม ถ้ามีเกณฑ์ที่สามก็ใส่ifแบบเดียวกันต่อไปเรื่อยๆ - อาเรย์ที่ส่งเข้าฟังก์ชันเป็นที่อยู่ของช่องแรกเสมอ ตามบทที่ 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;
}
- กรณีนี้ต้องตั้งชื่อ struct ไว้หลังคำว่า
structด้วย ไม่ใช่แค่ชื่อจากtypedefท้ายปีกกา - ตอนที่ compiler อ่านมาถึงบรรทัดนี้ ชื่อ
Nodeจากtypedefยังไม่เกิด เพราะอยู่หลังปีกกาปิด จึงต้องเรียกด้วยชื่อเต็มstruct Node 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
กลับไปเช็กรายการ ผ่านบทนี้เมื่อ แล้วไปทำแบบฝึกหัด