11.1 struct คืออะไร
ข้อมูลในโจทย์จริงมักมาเป็นชุด นักเรียนหนึ่งคนมีชื่อ คะแนน และห้อง จุดหนึ่งจุดมีพิกัด x กับ y สินค้าหนึ่งชิ้นมีรหัส จำนวน และราคา ข้อมูลในชุดเดียวกันเป็นคนละชนิดกันได้ ชื่อเป็นสตริง คะแนนเป็น int ราคาอาจเป็น long long หน้านี้รู้จัก struct (มาจาก structure แปลว่าโครงสร้าง) ซึ่งรวมข้อมูลหลายตัวที่เป็นของสิ่งเดียวกันไว้ในตัวแปรตัวเดียว
ทำไมต้องรวมเป็นก้อน
ถ้าไม่มี struct การเก็บนักเรียน 40 คนต้องใช้อาเรย์แยกกันหลายตัว แบบนี้
char names[40][21];
int scores[40];
int rooms[40];
ข้อมูลของคนที่ i กระจายอยู่สามที่ คือ names[i] scores[i] และ rooms[i] วิธีนี้เรียกว่า อาเรย์ขนาน (parallel arrays) ใช้ได้ แต่มีปัญหาเมื่อโปรแกรมใหญ่ขึ้น
- จะย้ายหรือสลับนักเรียนสองคน ต้องย้ายทั้งสามอาเรย์ ลืมอันเดียว ชื่อกับคะแนนก็สลับคนกันทันที และ compiler ไม่เตือนอะไรเลย
- จะเพิ่มข้อมูลอีกอย่าง เช่น อายุ ต้องตามแก้ทุกที่ที่ย้ายหรือคัดลอกข้อมูล
- ส่งนักเรียนหนึ่งคนเข้าฟังก์ชัน ต้องส่งทีละตัวครบทุกอาเรย์
struct แก้ทั้งสามข้อ เพราะข้อมูลของคนหนึ่งคนอยู่ในกล่องใหญ่กล่องเดียว ย้ายกล่องเดียวก็ย้ายครบทุกอย่าง
ประกาศ struct
#include <stdio.h>
struct Student { // (1)!
char name[21];
int score;
int room;
}; // (2)!
int main()
{
struct Student s; // (3)!
scanf("%s %d %d", s.name, &s.score, &s.room); // (4)!
printf("name = %s\n", s.name);
printf("score = %d\n", s.score);
printf("room = %d\n", s.room);
s.score = s.score + 5;
printf("after bonus = %d\n", s.score);
return 0;
}
- สร้าง ชนิดใหม่ ชื่อ
struct Studentข้างในมีสามช่อง แต่ละช่องเรียกว่า สมาชิก (member) บรรทัดนี้ยังไม่ได้สร้างตัวแปร เป็นแค่แบบพิมพ์ที่บอกว่ากล่องนักเรียนหน้าตาเป็นอย่างไร - ปีกกาปิดของ struct ต้องตามด้วย
;ต่างจากปีกกาของifและฟังก์ชัน ลืมแล้วจะได้ error ที่ชี้ผิดบรรทัด ดูในส่วนลองทำให้พัง - สร้างตัวแปร
sชนิดstruct Studentได้กล่องใหญ่หนึ่งกล่องที่มีสามช่องข้างใน s.nameเป็นอาเรย์ของcharจึงไม่ต้องมี&เหมือนในบทที่ 6 ส่วน&s.scoreคือที่อยู่ของช่องscoreเครื่องหมาย.ทำก่อน&จึงไม่ต้องใส่วงเล็บ
Mali 87 3
name = Mali
score = 87
room = 3
after bonus = 92
เข้าถึงสมาชิกด้วย จุด ตัวแปร.สมาชิก อ่านว่า "score ของ s" แต่ละสมาชิกใช้ได้เหมือนตัวแปรธรรมดาชนิดนั้นทุกอย่าง s.score เป็น int จึงบวก ลบ เปรียบเทียบ ส่งให้ scanf ได้ตามปกติ s.name เป็นสตริง จึงใช้ %s และฟังก์ชันใน string.h ได้ตามปกติ
ประกาศ struct ไว้นอก main ก่อนฟังก์ชันทุกตัว เพื่อให้ทุกฟังก์ชันในไฟล์รู้จักชนิดนี้ ซึ่งจะได้ใช้ในหน้า 11.4 ชื่อ struct นิยมขึ้นต้นด้วยตัวพิมพ์ใหญ่ เพื่อให้แยกออกจากชื่อตัวแปรได้ง่าย
กำหนดค่าตอนประกาศ
ใส่ค่าในปีกกาเรียงตามลำดับสมาชิกได้ เหมือนกำหนดค่าเริ่มต้นให้อาเรย์ในบทที่ 5 หรือจะระบุชื่อสมาชิกด้วยจุดนำหน้าก็ได้ แบบนี้ไม่ต้องจำลำดับ สมาชิกที่ไม่ได้ใส่ค่าจะเป็น 0 ทั้งหมด
#include <stdio.h>
struct Student {
char name[21];
int score;
int room;
};
int main()
{
struct Student a = {"Mali", 87, 3}; // (1)!
struct Student b = {.name = "Krit", .room = 2}; // (2)!
struct Student c = {0}; // (3)!
printf("%s %d %d\n", a.name, a.score, a.room);
printf("%s %d %d\n", b.name, b.score, b.room);
printf("[%s] %d %d\n", c.name, c.score, c.room);
return 0;
}
- เรียงตามลำดับที่ประกาศไว้ใน struct คือ name score room สลับลำดับเมื่อไร ค่าก็ลงผิดช่อง
- ระบุชื่อสมาชิก ไม่ต้องเรียงตามลำดับ
scoreไม่ได้ใส่ จึงเป็น 0 - ทุกสมาชิกเป็น 0 สตริงที่ทุกช่องเป็น 0 คือสตริงว่าง เพราะช่องแรกเป็น
'\0'ไปแล้ว
Mali 87 3
Krit 0 2
[] 0 0
ปีกกาแบบนี้ใช้ได้ เฉพาะตอนประกาศ เท่านั้น เหมือนอาเรย์ ถ้าประกาศไปแล้ว จะเขียน a = {"Ploy", 90, 1}; ไม่ได้ ต้องกำหนดทีละสมาชิก
คัดลอกทั้งก้อนด้วย =
จำได้ไหมว่าในบทที่ 6 สตริงใช้ = คัดลอกไม่ได้ ต้องใช้ strcpy แต่ struct คัดลอกด้วย = ได้ และคัดลอก ทุกสมาชิก รวมถึงอาเรย์ที่อยู่ข้างในด้วย
#include <stdio.h>
#include <string.h>
struct Student {
char name[21];
int score;
int room;
};
int main()
{
struct Student a = {"Mali", 87, 3};
struct Student b;
b = a;
strcpy(b.name, "Ploy");
b.score = 95;
printf("a: %s %d %d\n", a.name, a.score, a.room);
printf("b: %s %d %d\n", b.name, b.score, b.room);
return 0;
}
a: Mali 87 3
b: Ploy 95 3
บรรทัดที่ 14 ทำให้ b เป็นสำเนาของ a ทั้งก้อน หลังจากนั้นสองตัวนี้เป็นคนละกล่องกัน แก้ b แล้ว a ไม่เปลี่ยน b.room ยังเป็น 3 เพราะติดมาจากตอนคัดลอก สังเกตว่าแก้ชื่อใน b ยังต้องใช้ strcpy เพราะ b.name เป็นอาเรย์ กฎของสตริงยังใช้เหมือนเดิม ที่ = ใช้ได้คือคัดลอกทั้ง struct เท่านั้น
เรื่องนี้ทำให้งานที่ต้อง "จำข้อมูลของคนที่ดีที่สุดไว้" ง่ายขึ้นมาก เขียน best = s; บรรทัดเดียว ได้ทั้งชื่อ คะแนน และห้อง จะได้ใช้ในหน้าถัดไป
typedef ตั้งชื่อชนิดให้สั้นลง
พิมพ์ struct Student ทุกครั้งยาวเกินไป typedef ใช้ตั้งชื่อใหม่ให้ชนิดที่มีอยู่แล้ว รูปแบบคือ typedef ชนิดเดิม ชื่อใหม่;
#include <stdio.h>
typedef long long ll; // (1)!
typedef struct { // (2)!
int x;
int y;
} Point;
int main()
{
Point home = {3, 4};
Point school = {10, -2};
ll distance_squared = (ll)(school.x - home.x) * (school.x - home.x)
+ (ll)(school.y - home.y) * (school.y - home.y);
printf("home = (%d, %d)\n", home.x, home.y);
printf("school = (%d, %d)\n", school.x, school.y);
printf("distance squared = %lld\n", distance_squared);
return 0;
}
- ตั้งชื่อ
llให้ชนิดlong longโค้ดแข่งขันของคนอื่นใช้บรรทัดนี้บ่อยมาก เห็นแล้วจะได้อ่านออก - struct ไม่มีชื่อ แต่ตั้งชื่อชนิดว่า
Pointด้วยtypedefทำให้ประกาศตัวแปรด้วยPoint home;ได้เลย ไม่ต้องมีคำว่าstructนำหน้า
home = (3, 4)
school = (10, -2)
distance squared = 85
บรรทัดที่ 14 ถึง 15 หากำลังสองของระยะทางตามทฤษฎีบทพีทาโกรัส 7 × 7 + 6 × 6 = 85 ถ้าอยากได้ระยะทางจริงต้องถอดรากที่สอง ซึ่งได้ทศนิยม โจทย์แข่งขันจึงมักให้เทียบหรือตอบเป็นกำลังสองของระยะทาง ซึ่งเป็นจำนวนเต็มเสมอ สังเกต (ll) หน้าผลต่าง ถ้าผลต่างของพิกัดเกินประมาณ 46000 ผลคูณจะเกิน int แบบเดียวกับกับดัก int คูณ int ในบทที่ 1
ตั้งแต่นี้ไป เอกสารนี้ใช้แบบ typedef struct { ... } ชื่อ; เป็นหลัก มีกรณีเดียวที่ต้องตั้งชื่อ struct ไว้ด้วย คือ struct ที่มี pointer ชี้หาชนิดของตัวเอง ซึ่งจะเห็นในหน้า 11.4
ลองทำให้พัง
แบบที่ 1 ลืม ; หลังปีกกาปิด
ในโปรแกรม student ลบ ; หลัง } ของ struct ออก
student.c:9:1: error: expected ';', identifier or '(' before 'int'
9 | int main()
| ^~~
error ชี้ไปที่บรรทัด int main() ทั้งที่บรรทัดนั้นไม่ผิดอะไร เพราะ compiler อ่านต่อจาก } แล้วยังรอ ; อยู่ มารู้ตัวว่าขาดก็ตอนเจอคำว่า int ในบรรทัดถัดไป ถ้า error ชี้บรรทัดที่ดูถูกต้องทุกอย่าง ให้ย้อนดูบรรทัดก่อนหน้าเสมอ
แบบที่ 2 ใช้ชื่อ struct โดยไม่มีคำว่า struct
ประกาศแบบไม่มี typedef คือ struct Point { int x; int y; }; แล้วใน main เขียน
Point p = {3, 4};
point.c:10:5: error: unknown type name 'Point'; use 'struct' keyword to refer to the type
10 | Point p = {3, 4};
| ^~~~~
| struct
ถ้าไม่ได้ใช้ typedef ชื่อของชนิดคือ struct Point ทั้งสองคำ compiler บอกวิธีแก้มาให้แล้วว่าให้เติม struct ข้างหน้า ถัดจากบรรทัดนี้จะมี error อีกหลายอันเรื่อง p.x กับ p.y ซึ่งเกิดตามมาเพราะ p ประกาศไม่สำเร็จ แก้ตัวแรกแล้วหายหมด
แบบที่ 3 เทียบ struct ด้วย ==
struct Point a = {3, 4};
struct Point b = {3, 4};
if (a == b) {
printf("same\n");
}
compare.c:12:11: error: invalid operands to binary == (have 'struct Point' and 'struct Point')
12 | if (a == b) {
| ^~
struct คัดลอกด้วย = ได้ แต่ เปรียบเทียบด้วย == ไม่ได้ และใช้ < > ไม่ได้ด้วย ต้องเทียบทีละสมาชิกเอง เช่น a.x == b.x && a.y == b.y เพราะ compiler ไม่รู้ว่าเราอยากให้สอง struct "เท่ากัน" ในความหมายไหน ถ้าเป็นนักเรียน อาจหมายถึงชื่อเดียวกัน หรือคะแนนเท่ากันก็ได้ ในหน้า 11.4 จะเขียนฟังก์ชันเปรียบเทียบเองตามที่โจทย์กำหนด
สรุป
- struct รวมข้อมูลหลายชนิดที่เป็นของสิ่งเดียวกันไว้ในตัวแปรตัวเดียว แทนการใช้อาเรย์ขนานหลายตัว
- ประกาศ struct นอก
mainและปีกกาปิดต้องตามด้วย; - เข้าถึงสมาชิกด้วยจุด
s.scoreแต่ละสมาชิกใช้ได้เหมือนตัวแปรชนิดนั้น - กำหนดค่าด้วยปีกกาได้เฉพาะตอนประกาศ
{0}ทำให้ทุกสมาชิกเป็น 0 =คัดลอกทั้งก้อนรวมอาเรย์ข้างใน แต่==ใช้ไม่ได้ ต้องเทียบทีละสมาชิกtypedef struct { ... } Point;ทำให้เขียนPoint p;ได้โดยไม่ต้องมีคำว่าstruct