[0]zero-index clytie0x/zero-index

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 แก้ทั้งสามข้อ เพราะข้อมูลของคนหนึ่งคนอยู่ในกล่องใหญ่กล่องเดียว ย้ายกล่องเดียวก็ย้ายครบทุกอย่าง

s (struct Student) name "Mali" score 87 room 3 s.name s.score s.room

ประกาศ 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;
}
  1. สร้าง ชนิดใหม่ ชื่อ struct Student ข้างในมีสามช่อง แต่ละช่องเรียกว่า สมาชิก (member) บรรทัดนี้ยังไม่ได้สร้างตัวแปร เป็นแค่แบบพิมพ์ที่บอกว่ากล่องนักเรียนหน้าตาเป็นอย่างไร
  2. ปีกกาปิดของ struct ต้องตามด้วย ; ต่างจากปีกกาของ if และฟังก์ชัน ลืมแล้วจะได้ error ที่ชี้ผิดบรรทัด ดูในส่วนลองทำให้พัง
  3. สร้างตัวแปร s ชนิด struct Student ได้กล่องใหญ่หนึ่งกล่องที่มีสามช่องข้างใน
  4. 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;
}
  1. เรียงตามลำดับที่ประกาศไว้ใน struct คือ name score room สลับลำดับเมื่อไร ค่าก็ลงผิดช่อง
  2. ระบุชื่อสมาชิก ไม่ต้องเรียงตามลำดับ score ไม่ได้ใส่ จึงเป็น 0
  3. ทุกสมาชิกเป็น 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;
}
  1. ตั้งชื่อ ll ให้ชนิด long long โค้ดแข่งขันของคนอื่นใช้บรรทัดนี้บ่อยมาก เห็นแล้วจะได้อ่านออก
  2. 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