Sign in

Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)

2026-10-22 · updated 2026-10-30 · 21 min read

บทที่ 9 โครงสร้างข้อมูลแบบระเบียน (struct)

ShareFacebookXLinkedInLINE

แผนบริหารการสอนประจำบทที่ 9

บทนี้ว่าด้วยโครงสร้างข้อมูลแบบระเบียน (struct) ของภาษา C ซึ่งเป็นโครงสร้างข้อมูลที่รวมข้อมูลต่างชนิดกันไว้ในระเบียนเดียว ต่อยอดจากอาร์เรย์ในบทที่ 7 ฟังก์ชันในบทที่ 6 และการจัดการสตริงในบทที่ 8 โดยอธิบายการนิยาม struct การเข้าถึงสมาชิกด้วยตัวดำเนินการจุด การใช้อาร์เรย์ของระเบียนและวนประมวลผลสมาชิกด้วยลูป และการส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่า (by value) เพื่อให้จัดกลุ่มข้อมูลที่สัมพันธ์กันและประมวลผลได้อย่างเป็นระบบ

ผลลัพธ์ข้อความหลักฐานCLO
LLO9.1นิยามโครงสร้างข้อมูลแบบระเบียน (struct) และเข้าถึงสมาชิกด้วยตัวดำเนินการจุดงานข้อ 1 (ปฏิบัติการที่ 9 Lab A)CLO1
LLO9.2ใช้อาร์เรย์ของระเบียนและวนประมวลผลสมาชิกได้งานข้อ 2 (ปฏิบัติการที่ 9 Lab B)CLO2
LLO9.3ส่งระเบียนเข้าฟังก์ชันเพื่อแยกงานเป็นส่วนย่อยงานข้อ 3 (ปฏิบัติการที่ 9 Lab A/B)CLO2
LLO9.4ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้โค้ดทุกข้อที่ส่งในบทนี้CLO5

CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):

  • CLO1 อธิบายและใช้ตัวแปร ชนิดข้อมูล การรับ-แสดงผล และโครงสร้างควบคุม (if/else, switch, loop) เพื่อแก้ปัญหาและได้ผลลัพธ์ที่ถูกต้อง
  • CLO2 อธิบายขั้นตอนการทำงาน (Logic) ของภาษาโปรแกรมและเลือกใช้โครงสร้างควบคุม (Control Structure) ให้สอดคล้องกับโจทย์ที่กำหนดได้

บทที่ 9 เป็นจุดที่นำอาร์เรย์จากบทที่ 7 ฟังก์ชันจากบทที่ 6 และสตริงจากบทที่ 8 มาประกอบกันเป็นระเบียนที่รวมข้อมูลหลายชนิดของสิ่งเดียวกัน จึงเป็นสะพานเชื่อมไปสู่การเรียงลำดับและการค้นหาข้อมูลที่เป็นระเบียนในบทที่ 10 และโครงงานบูรณาการในบทที่ 15 งานทั้งสามข้อในบทนี้สะสมหลักฐานของ CLO1 และ CLO2 ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท

วิธีสอน

  1. ทบทวนอาร์เรย์จากบทที่ 7 ที่เก็บข้อมูลชนิดเดียวกันหลายค่า แล้วตั้งโจทย์ที่ต้องเก็บข้อมูลต่างชนิดของสิ่งเดียวกัน เช่น ชื่อ รหัส และเกรดเฉลี่ยของนักศึกษาหนึ่งคน เพื่อชี้ให้เห็นความจำเป็นของระเบียนแทนการประกาศตัวแปรแยกหลายตัว
  2. แนะนำการนิยาม struct และการเข้าถึงสมาชิกด้วยตัวดำเนินการจุด โดยสาธิตบนเครื่องจริง และย้ำการกำหนดค่าสตริงด้วย strcpy จากบทที่ 8 แทนการใช้เครื่องหมายเท่ากับ
  3. สาธิตการสร้างอาร์เรย์ของระเบียนและการวนประมวลผลสมาชิกด้วยลูป for จากบทที่ 5 เพื่อแสดงและสรุปข้อมูล เช่น ค่าเฉลี่ยของเกรด
  4. อธิบายการส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่า และชี้ว่าการแก้สมาชิกภายในฟังก์ชันไม่กระทบระเบียนเดิม เช่นเดียวกับการส่งตัวแปรเดี่ยวในบทที่ 6
  5. ให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที

สื่อการเรียนการสอน

  • เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
  • เอกสารประกอบบทที่ 9 และสไลด์สรุปการนิยาม struct การเข้าถึงสมาชิก อาร์เรย์ของระเบียน และการส่งระเบียนเข้าฟังก์ชัน
  • ไฟล์โปรแกรมตัวอย่าง ch9_struct.c

การวัดผลและการประเมินผล

กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 10 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 10 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)

ข้องานหลักฐานผลลัพธ์CLO
1นิยาม struct Student (name, id, gpa) กำหนดค่าและแสดงสมาชิกด้วยตัวดำเนินการจุดไฟล์ซอร์สโค้ด + ผลการรันLLO9.1CLO1
2สร้างอาร์เรย์ของระเบียนแล้ววนคำนวณค่าเฉลี่ยเกรดไฟล์ซอร์สโค้ด + ผลการรันLLO9.2CLO2
3เขียนฟังก์ชันรับระเบียนแล้วแสดงผล แยกงานการแสดงออกจาก mainไฟล์ซอร์สโค้ด + ผลการรันLLO9.3CLO2

เงื่อนไขผ่าน: งานข้อ 3 ประเมินจากไฟล์เดียวกับงานข้อ 1 และข้อ 2 ซึ่งใช้ฟังก์ชัน print_student ร่วมกัน ดังนั้นงานข้อ 1 ต้องนิยาม struct และเข้าถึงสมาชิกถูกต้องและคอมไพล์ผ่านก่อนจึงจะประเมินข้อ 2 และข้อ 3 ได้

คะแนนใบงาน (10 คะแนน)

ผลลัพธ์สิ่งที่ตรวจคะแนน
LLO9.1งานข้อที่ระบุผลลัพธ์นี้3
LLO9.2งานข้อที่ระบุผลลัพธ์นี้3
LLO9.3งานข้อที่ระบุผลลัพธ์นี้3
LLO9.4มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง1

ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา

เกณฑ์ให้คะแนน

งานข้อ 1 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:

เกณฑ์ผ่านยังไม่ผ่าน
งานข้อ 1นิยาม struct และเข้าถึงสมาชิกด้วยจุดถูกต้อง กำหนดค่าสตริงด้วย strcpy และแสดงสมาชิกครบนิยามผิด เข้าถึงสมาชิกผิด หรือกำหนดค่าชื่อด้วยเครื่องหมายเท่ากับ

งานข้อ 2 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ (checklist): สร้างอาร์เรย์ของระเบียนถูก, วนประมวลผลอยู่ในช่วงดัชนี 0 ถึง n-1 ไม่เกินขอบเขต, ค่าเฉลี่ยเกรดถูกต้องและแสดงด้วย %.2f, จัดย่อหน้า 4 ช่องว่างและมีคอมเมนต์ส่วนหัว

งานข้อ 3 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ: ฟังก์ชันรับระเบียนเป็นพารามิเตอร์ถูกต้อง, แยกงานการแสดงผลออกจาก main, คอมไพล์ผ่านไม่มีการแจ้งเตือน, เป็นไปตามมาตรฐานโค้ด

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

บทนำ

โปรแกรมจำนวนมากต้องจัดเก็บข้อมูลต่างชนิดกันที่อธิบายสิ่งเดียวกัน เช่น นักศึกษาหนึ่งคนมีทั้งชื่อที่เป็นสตริง รหัสที่เป็นจำนวนเต็ม และเกรดเฉลี่ยที่เป็นทศนิยม อาร์เรย์ในบทที่ 7 เก็บได้เฉพาะข้อมูลชนิดเดียวกันหลายค่า จึงไม่เหมาะกับการรวมข้อมูลต่างชนิดของสิ่งเดียวกัน โครงสร้างข้อมูลแบบระเบียน (struct) เข้ามาแก้ปัญหานี้ด้วยการรวมข้อมูลต่างชนิดไว้ในระเบียนเดียวภายใต้ชื่อเดียว แล้วเข้าถึงแต่ละสมาชิกด้วยตัวดำเนินการจุด บทที่ 6 ได้แนะนำฟังก์ชัน บทที่ 7 ได้แนะนำอาร์เรย์ และบทที่ 8 ได้แนะนำการจัดการสตริงไปแล้ว บทนี้นำทั้งสามเรื่องมาประกอบกันเป็นระเบียน ทั้งระเบียนเดี่ยว อาร์เรย์ของระเบียน และการส่งระเบียนเข้าฟังก์ชัน

ทักษะเรื่องระเบียนในบทนี้เป็นพื้นฐานของบทที่ตามมา การเก็บข้อมูลที่เป็นระเบียนหลายรายการในอาร์เรย์จะต่อยอดเป็นการเรียงลำดับและการค้นหาในบทที่ 10 และการออกแบบโครงสร้างข้อมูลของโปรแกรมในโครงงานบูรณาการในบทที่ 15

สิ่งที่ได้จากบทนี้ใช้ต่อในบท
การนิยาม struct และการเข้าถึงสมาชิกด้วยตัวดำเนินการจุดบท 10 (เรียง/ค้นหา), บท 15 (โครงงาน)
อาร์เรย์ของระเบียนและการวนประมวลผลสมาชิกบท 10 (เรียง/ค้นหา), บท 15 (โครงงาน)
การส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่าบท 10 เป็นต้นไป

ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์

บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE และต้องมีความรู้เรื่องอาร์เรย์จากบทที่ 7 ฟังก์ชันและการส่งค่าแบบส่งสำเนาค่าจากบทที่ 6 การจัดการสตริงและ strcpy จากบทที่ 8 และการวนซ้ำด้วยลูป for จากบทที่ 5 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง

เครื่องมือรุ่นที่ทดสอบหน้าที่
คอมไพเลอร์ gcc13.3.0 (Ubuntu)แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ
มาตรฐานภาษาC11 (-std=c11)กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง
⚠️Warning

โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ผลการแสดงข้อความภาษาไทยอาจต่างกันเล็กน้อย แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน

วิธีใช้บทเรียนและใบงานในหน้านี้

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

ใบงานเป็นตารางสำหรับคัดลงสมุดหรือเอกสารของตน ไม่ใช่แบบฟอร์มส่งคำตอบบนเว็บไซต์ ใช้ลำดับ ออกแบบสมาชิก → ทำนายสถานะ → คอมไพล์ → รัน → ตรวจว่าข้อมูลยังจับคู่ถูกต้อง

ชื่อ รหัส GPA และคะแนนทั้งหมดเป็นข้อมูลสมมติ ไม่ใช่ข้อมูลผู้เรียนจริง กิจกรรมเพิ่มไม่เปลี่ยน LLO9.1–9.3 งานส่ง หรือคะแนนรายวิชา แผนเวลาและเฉลยอยู่เฉพาะส่วนผู้สอน

แต่ละไฟล์ตัวอย่างเป็นโปรแกรมแยกกัน นิยาม Student แบบ name/id/gpa กับแบบ code/name/score เป็นคนละแบบจำลองในคนละไฟล์ ไม่คัดนิยามทั้งสองมาต่อกันในไฟล์เดียวแล้วคาดว่าจะกลายเป็นชนิดเดียวกัน

9.1 แนวคิดระเบียนและการนิยาม struct

ระดับที่ 1: โครงสร้างข้อมูลแบบระเบียน (struct) คือโครงสร้างข้อมูลที่รวมข้อมูลต่างชนิดกันไว้ในระเบียนเดียวภายใต้ชื่อเดียว ต่างจากอาร์เรย์ในบทที่ 7 ที่เก็บได้เฉพาะข้อมูลชนิดเดียวกันหลายค่า ระเบียนจึงเหมาะกับการจัดกลุ่มข้อมูลที่สัมพันธ์กันของสิ่งเดียวกัน เช่น ข้อมูลของนักศึกษาหนึ่งคน

ระดับที่ 2: การนิยาม struct ใช้คำสำคัญ struct ตามด้วยชื่อระเบียนและรายการสมาชิกในวงเล็บปีกกา โดยแต่ละสมาชิกระบุชนิดข้อมูลและชื่อ และต้องปิดท้ายนิยามด้วยเครื่องหมายอัฒภาค ตัวอย่างระเบียนนักศึกษาที่มีชื่อ รหัส และเกรดเฉลี่ย นิยามได้ดังนี้

C
struct Student {
    char name[20];
    long id;
    double gpa;
};

เมื่อนิยามแล้ว การประกาศตัวแปรชนิดระเบียนใช้ struct Student a; ซึ่งได้ตัวแปร a ที่มีสมาชิกครบทั้งสามตัว

ระดับที่ 3: เหตุที่ struct มีประโยชน์คือช่วยจัดกลุ่มข้อมูลที่สัมพันธ์กันไว้ด้วยกันแทนการประกาศตัวแปรแยกหลายตัว เช่น name, id, และ gpa แยกกัน การรวมเป็นระเบียนทำให้ส่งต่อข้อมูลทั้งชุดเป็นหน่วยเดียวได้ง่ายขึ้น และสื่อความหมายว่าข้อมูลเหล่านี้เป็นของสิ่งเดียวกัน ข้อผิดพลาดที่พบบ่อยคือการลืมเครื่องหมายอัฒภาคท้ายนิยาม struct ซึ่งทำให้คอมไพล์ไม่ผ่าน

คำถามตรวจความเข้าใจ 9.1: struct ต่างจากอาร์เรย์อย่างไรในแง่ชนิดข้อมูลของสมาชิก

ใบงาน 1: เลือกสมาชิกตามความหมายของข้อมูล

ออกแบบระเบียนอุปกรณ์หนึ่งรายการก่อนพิมพ์โค้ด แยกรหัสที่ใช้ระบุตัวตนจากจำนวนที่นำไปคำนวณ รหัส "0017" ควรรักษาศูนย์นำหน้า ส่วนจำนวนคงเหลือใช้เป็นตัวเลขได้

ข้อมูลชนิดที่เสนอหน่วยหรือความหมายขอบเขตที่ยอมรับตัวอย่างผิด
รหัสอุปกรณ์
ชื่ออุปกรณ์
จำนวนคงเหลือ
จำนวนต่อชุด

ระบุพื้นที่ชื่อรวมตัวปิดและจำนวนรายการสูงสุดด้วย การมี struct ไม่ทำให้ชื่อยาวเท่าใดก็ได้ และจำนวนต่อชุดยังต้องเป็นบวกก่อนนำไปหาร

ตัวอย่าง Lab เดิมใช้รหัสสมมติ 65001 จึงปรับสมาชิก id เป็น long พร้อมรูปแบบ %ld เพื่อรองรับค่านี้ตามช่วงขั้นต่ำที่ C11 รับประกัน ส่วนตัวอย่างรหัสที่ศูนย์นำหน้าต้องคงรูปใช้ char array

9.2 การเข้าถึงสมาชิก

ระดับที่ 1: การเข้าถึงสมาชิกของระเบียนใช้ตัวดำเนินการจุด (dot operator) คือชื่อตัวแปรระเบียนตามด้วยจุดและชื่อสมาชิก เพื่ออ่านหรือกำหนดค่าให้สมาชิกนั้น

ระดับที่ 2: การกำหนดค่าสมาชิกที่เป็นจำนวนเต็มหรือทศนิยมใช้เครื่องหมายเท่ากับได้โดยตรง เช่น a.id = 65001L; และ a.gpa = 3.75; ส่วนสมาชิกที่เป็นสตริง (อาร์เรย์ของอักขระ) ต้องกำหนดค่าด้วย strcpy จากบทที่ 8 เช่น strcpy(a.name, "Somchai"); ซึ่งต้องรวมส่วนหัว string.h ไว้ด้วย การอ่านค่าสมาชิกก็ใช้ตัวดำเนินการจุดเช่นกัน เช่น a.gpa ในการแสดงผล

C
struct Student a;
strcpy(a.name, "Somchai");
a.id = 65001L;
a.gpa = 3.75;

ระดับที่ 3: นอกจากกำหนดค่าทีละสมาชิกแล้ว ยังกำหนดค่าเริ่มต้นพร้อมประกาศได้ด้วยวงเล็บปีกกา โดยเรียงค่าตามลำดับสมาชิกในนิยาม เช่น {"Anong", 65002, 3.90} กำหนดชื่อ รหัส และเกรดเฉลี่ยตามลำดับ ข้อผิดพลาดที่พบบ่อยคือการพยายามกำหนดค่าชื่อด้วยเครื่องหมายเท่ากับ เช่น a.name = "Somchai"; ซึ่งใช้ไม่ได้กับสมาชิกที่เป็นอาร์เรย์ของอักขระ ต้องใช้ strcpy แทน

คำถามตรวจความเข้าใจ 9.2: การกำหนดค่าให้สมาชิกที่เป็นชื่อ (สตริง) ของระเบียนต้องใช้คำสั่งใด และเหตุใดจึงกำหนดด้วยเครื่องหมายเท่ากับไม่ได้

ใบงาน 2: จุดประกาศชนิด จุดสร้างตัวแปร และจุดอ่านสมาชิก

นิยาม struct Student { ... }; บอกองค์ประกอบของชนิด แต่ยังไม่ใช่การรับข้อมูลครบหนึ่งรายการ ให้ทำเครื่องหมายแยกจาก struct Student a; และคำสั่งกำหนดค่าสมาชิกก่อนพิมพ์

ขั้นมีตัวแปรระเบียนแล้วหรือยังสมาชิกใดพร้อมนำไปแสดง
หลังนิยามชนิด
หลังประกาศ local a โดยไม่ตั้งค่า
หลังคัดลอกชื่อและกำหนด id
หลังกำหนด gpa ครบ

การกำหนดค่าเริ่มต้นด้วยวงเล็บปีกกาต้องตรงลำดับสมาชิกในนิยาม ส่วนการกำหนดค่าภายหลังด้วยจุดระบุชื่อสมาชิกชัดกว่า อย่าสลับชื่อ รหัส หรือคะแนนแล้วพยายามอธิบายจากค่าที่บังเอิญพิมพ์ได้

อาร์เรย์ชื่อภายในระเบียนยังคัดลอกด้วย a.name = "..." ไม่ได้ แต่การกำหนดค่าทั้งระเบียนชนิดเดียวกันทำได้ ใบงานถัดไปจะแยกสองกรณีนี้ด้วยผลรัน

9.3 อาร์เรย์ของระเบียน

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

ระดับที่ 2: การประกาศอาร์เรย์ของระเบียนระบุชนิดระเบียน ชื่ออาร์เรย์ และจำนวนสมาชิก เช่น struct Student room[2]; และกำหนดค่าเริ่มต้นด้วยวงเล็บปีกกาซ้อน โดยแต่ละระเบียนอยู่ในวงเล็บปีกกาของตนเอง การเข้าถึงสมาชิกของระเบียนตัวที่ i ใช้ดัชนีก่อนแล้วจึงตามด้วยตัวดำเนินการจุด เช่น room[i].gpa การวนทุกระเบียนจึงใช้ลูป for จากบทที่ 5

C
struct Student room[2] = {
    {"Anong", 65002L, 3.90},
    {"Decha", 65003L, 2.85}
};
for (int i = 0; i < 2; i++) {
    sum += room[i].gpa;
}

ระดับที่ 3: อาร์เรย์ของระเบียนมักใช้ร่วมกับการสรุปข้อมูล เช่น การหาผลรวมหรือค่าเฉลี่ยของสมาชิกบางตัวจากทุกระเบียน โดยใช้ตัวแปรสะสมจากบทที่ 5 เช่น sum += room[i].gpa; แล้วหารด้วยจำนวนสมาชิกเพื่อหาค่าเฉลี่ย ข้อควรระวังเช่นเดียวกับอาร์เรย์ในบทที่ 7 คือการวนไม่ให้เกินขอบเขตดัชนี โดยอาร์เรย์ขนาด n มีดัชนีที่ใช้ได้ตั้งแต่ 0 ถึง n-1

คำถามตรวจความเข้าใจ 9.3: การเข้าถึงเกรดเฉลี่ย (gpa) ของสมาชิกตัวที่ i ในอาร์เรย์ของระเบียนเขียนอย่างไร

9.4 การส่งระเบียนเข้าฟังก์ชัน

ระดับที่ 1: ฟังก์ชันจากบทที่ 6 สามารถรับระเบียนเป็นพารามิเตอร์ได้ เพื่อแยกงาน เช่น การแสดงผลระเบียน ออกเป็นฟังก์ชันที่นำกลับมาใช้ซ้ำได้

ระดับที่ 2: การประกาศพารามิเตอร์ที่เป็นระเบียนใช้ชนิดระเบียนตามด้วยชื่อพารามิเตอร์ เช่น void print_student(struct Student s) รับระเบียนนักศึกษา s แล้วแสดงสมาชิกด้วยตัวดำเนินการจุดภายในฟังก์ชัน เมื่อเรียกใช้ก็ส่งตัวแปรระเบียนเข้าไป เช่น print_student(a); หรือ print_student(room[i]);

C
void print_student(struct Student s) {
    printf("รหัส %ld: %s (GPA %.2f)\n", s.id, s.name, s.gpa);
}

ระดับที่ 3: การส่งระเบียนเข้าฟังก์ชันในบทนี้เป็นการส่งแบบส่งสำเนาค่า (by value) เช่นเดียวกับการส่งตัวแปรเดี่ยวในบทที่ 6 คือฟังก์ชันได้รับสำเนาของระเบียน การแก้สมาชิกภายในฟังก์ชันจึงไม่กระทบระเบียนเดิมที่ main ซึ่งต่างจากการส่งอาร์เรย์เข้าฟังก์ชันในบทที่ 7 ที่ทำงานกับอาร์เรย์เดิมโดยตรง การแยกงานการแสดงผลออกเป็นฟังก์ชัน print_student ช่วยให้โปรแกรมอ่านง่ายและลดการเขียนซ้ำ ซึ่งสอดคล้องกับมาตรฐานคุณภาพโค้ด

คำถามตรวจความเข้าใจ 9.4: เมื่อส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่า การแก้สมาชิกภายในฟังก์ชันกระทบระเบียนเดิมที่ main หรือไม่ เพราะเหตุใด

9.5 กรณีศึกษาต่อเนื่อง: ผูกข้อมูลที่เป็นรายการเดียวกันเข้าด้วยกัน

หากเก็บรหัส ชื่อ และคะแนนในอาร์เรย์สามชุด ต้องรักษาว่าดัชนีเดียวกันเป็นรายการเดียวกันเสมอ เมื่อสลับคะแนนเพื่อเรียงลำดับแต่ลืมสลับชื่อ ผลรายงานอาจนำคะแนนของคนหนึ่งไปแสดงข้างอีกชื่อ struct ช่วยรวมข้อมูลที่เป็นหน่วยเดียวกันให้เคลื่อนย้ายร่วมกันได้

ใช้ข้อมูลสมมติทั้งหมดในตัวอย่าง ไม่ใช้ชื่อ รหัส หรือคะแนนของผู้เรียนจริง รหัสเป็นข้อความเพื่อคงเลขศูนย์นำหน้า ส่วนคะแนนเป็นจำนวนที่นำไปคำนวณ การเลือกชนิดตามความหมายจึงต่อเนื่องจากบทที่ 2 และบทสตริง

C
#include <stdio.h>
 
struct Student {
    char code[5];
    char name[16];
    int score;
};
 
struct Student with_bonus(struct Student student, int bonus)
{
    student.score += bonus;
    return student;
}
 
int main(void)
{
    struct Student original = {"0017", "Sample-A", 70};
    struct Student adjusted = with_bonus(original, 5);
 
    printf("%s %s %d\n",
           original.code, original.name, original.score);
    printf("%s %s %d\n",
           adjusted.code, adjusted.name, adjusted.score);
    return 0;
}

ผลบรรทัดแรกเป็น 0017 Sample-A 70 และบรรทัดที่สองเป็น 0017 Sample-A 75 การส่งระเบียนนี้เข้า with_bonus เป็นการส่งค่า การแก้สมาชิก student.score ในฟังก์ชันจึงไม่แก้ original โดยตรง ฟังก์ชันคืนระเบียนที่ปรับแล้วให้เก็บใน adjusted ตัวอย่างกำหนดค่าขนาดเล็กและโบนัสแน่นอน หากใช้ทั่วไปต้องกำหนดขอบเขตคะแนนและการจัดการเกินช่วงเพิ่ม

9.5.1 การคัดลอกทั้งระเบียนกับอาร์เรย์เดี่ยวต่างกันอย่างไร

คำสั่ง struct Student copy = original; คัดลอกสมาชิกของระเบียน รวมเนื้อหาอาร์เรย์ code และ name ภายในระเบียนด้วย จึงแก้ copy.name[0] ได้โดยไม่เปลี่ยนอาร์เรย์ชื่อของ original ในตัวอย่างที่สมาชิกเป็นอาร์เรย์เก็บข้อมูลโดยตรง

ส่วนการเขียน copy.name = original.name; ภายหลังไม่ใช่การกำหนดค่าอาร์เรย์ที่ C อนุญาต ต้องใช้วิธีคัดลอกสตริงพร้อมตรวจขนาด หรือกำหนดค่าทั้งระเบียนตามงานที่ต้องการ หลักการคัดลอกระเบียนอ้างอิงจาก GNU C: Structure Assignment

ข้อสรุปนี้จำกัดกับโครงสร้างในบทที่มีอาร์เรย์อยู่ภายใน หากระเบียนในอนาคตมีพอยน์เตอร์ การคัดลอกสมาชิกพอยน์เตอร์ไม่ได้คัดลอกข้อมูลปลายทางใหม่ทั้งหมด บทนี้จึงไม่ใช้คำว่า “deep copy ทุกกรณี” เพื่ออธิบายการกำหนดค่า struct

ตัวอย่างพร้อมใช้: คัดลอกทั้งระเบียนแล้วแก้เฉพาะสำเนา

สร้างไฟล์ record_copy.c รับโบนัสช่วง 0–10 เพื่อให้คะแนนสมมติเริ่มต้น 70 บวกแล้วไม่เกิน 80 ตัวอย่างใช้เฉพาะสมาชิกอาร์เรย์ที่เก็บข้อมูลภายใน ไม่มีสมาชิกพอยน์เตอร์

C
/* record_copy.c : embedded arrays are part of the record value */
#include <stdio.h>
 
struct Student {
    char code[5];
    char name[16];
    int score;
};
 
struct Student with_bonus(struct Student student, int bonus);
 
int main(void)
{
    int bonus;
    if (scanf("%d", &bonus) != 1) {
        puts("Input error");
        return 1;
    }
    if (bonus < 0 || bonus > 10) {
        puts("Range error");
        return 1;
    }
 
    struct Student original = {"0017", "Sample-A", 70};
    struct Student copy = original;
    copy.name[0] = 'X';
    copy.score = 0;
    struct Student adjusted = with_bonus(original, bonus);
 
    printf("original=%s %s %d\n",
           original.code, original.name, original.score);
    printf("copy=%s %s %d\n", copy.code, copy.name, copy.score);
    printf("adjusted=%s %s %d\n",
           adjusted.code, adjusted.name, adjusted.score);
    return 0;
}
 
/* The caller supplies score 70 and bonus 0 through 10. */
struct Student with_bonus(struct Student student, int bonus)
{
    student.score += bonus;
    return student;
}
โบนัสที่ลองชื่อและคะแนน originalชื่อและคะแนน copyชื่อและคะแนน adjusted
0
5
10

อธิบายจากตำแหน่งเก็บสามระเบียน ไม่ใช่คิดว่าชื่อ Student ทำให้ทุกตัวเป็นรายการเดียวกัน ถ้าเรียก with_bonus แล้วไม่เก็บค่าคืน original จะไม่เปลี่ยน และการแก้ copy.name ไม่แก้ชื่อเดิมในตัวอย่างนี้

ข้อสรุปเรื่องอาร์เรย์ภายในไม่ใช่คำรับรองว่าการคัดลอก struct ที่มีพอยน์เตอร์จะสร้างสำเนาข้อมูลปลายทางใหม่ด้วย รายละเอียดนั้นต้องมีสัญญาการจัดการพื้นที่อีกแบบ

ตัวอย่างสลับรายการ: เคลื่อนย้ายทั้งรหัส ชื่อ และคะแนน

สร้างไฟล์ swap_records.c รับดัชนีสองตำแหน่งในช่วง 0–2 แล้วสลับทั้งระเบียน ใช้ข้อมูลที่คะแนนต่างกันเพื่อให้เห็นว่าคะแนนยังอยู่คู่กับรหัสและชื่อเดิม

C
/* swap_records.c : swap whole records, not only scores */
#include <stdio.h>
 
struct Student {
    char code[5];
    char name[16];
    int score;
};
 
void print_room(const struct Student room[], int count);
 
int main(void)
{
    struct Student room[3] = {
        {"0017", "Sample-A", 70},
        {"0018", "Sample-B", 90},
        {"0019", "Sample-C", 80}
    };
    int left;
    int right;
    if (scanf("%d %d", &left, &right) != 2) {
        puts("Input error");
        return 1;
    }
    if (left < 0 || left > 2 || right < 0 || right > 2) {
        puts("Range error");
        return 1;
    }
    struct Student temporary = room[left];
    room[left] = room[right];
    room[right] = temporary;
    print_room(room, 3);
    return 0;
}
 
void print_room(const struct Student room[], int count)
{
    for (int i = 0; i < count; ++i) {
        printf("%s %s %d\n", room[i].code, room[i].name, room[i].score);
    }
}

ทำนายเมื่อสลับ 0 กับ 2, 0 กับ 1 และ 1 กับ 1 โดยเขียนตารางก่อน/หลังให้ครบสามสมาชิก การสลับเพียง room[0].score กับ room[2].score จะเปลี่ยนความสัมพันธ์ข้อมูล ไม่ใช่การย้ายรายการเดิมทั้งรายการ

กิจกรรมนี้ฝึกย้ายระเบียนหนึ่งคู่ ยังไม่ใช่อัลกอริทึมเรียงลำดับทั้งชุด เก็บหลักฐานความสัมพันธ์นี้ไว้ใช้เมื่อเรียงข้อมูลในบทที่ 10

9.6 อาร์เรย์ของระเบียนและการรักษาความสัมพันธ์ของข้อมูล

9.6.1 เดินทีละรายการแล้วอ่านสมาชิกที่ต้องใช้

กำหนดข้อมูลสมมติสามรายการเป็นรหัส 0017 คะแนน 70, รหัส 0018 คะแนน 90 และรหัส 0019 คะแนน 80 การหาผลรวมอ่าน students[i].score ส่วนการแสดงผลอ่านรหัส ชื่อ และคะแนนจาก students[i] เดียวกัน ดัชนีเลือกว่ารายการใด จุดเลือกว่าสมาชิกใด ไม่ควรสลับบทบาทสองส่วนนี้

ในการหาคะแนนสูงสุด ให้จำดัชนีของระเบียนที่ดีที่สุด แล้วเมื่อพบค่าที่มากกว่าให้เปลี่ยนดัชนี หลังจบลูปจึงแสดงทั้งชื่อและคะแนนจากระเบียนเดียวกัน หากจำเพียงคะแนนสูงสุดแต่พยายามแสดงชื่อจากตัวแปรลูปหลังจบ อาจได้ชื่อผิดหรือเข้าถึงนอกขอบเขต

9.6.2 ตัดสินกติกาข้อมูลซ้ำก่อนเขียนเงื่อนไข

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

ตัวอย่างตารางตรวจรับระเบียนใหม่ประกอบด้วยรหัสถูกแบบ ชื่อไม่ว่าง คะแนนอยู่ในช่วง และรหัสยังไม่ซ้ำ ต้องตรวจทั้งหมดก่อนเปลี่ยนข้อมูลถาวรในอาร์เรย์ มิฉะนั้นอาจเพิ่ม count ไปแล้วแต่รายการมีข้อมูลไม่ครบ การแยก “เตรียมระเบียนชั่วคราว → ตรวจ → เก็บ” ช่วยรักษาสถานะเดิมเมื่อปฏิเสธ

ตัวอย่างค้นหารหัส: แสดงสมาชิกจากระเบียนเดียวกัน

สร้างไฟล์ find_record.c รับรหัสสี่หลัก โดยใช้ fgets ในบัฟเฟอร์ที่มีที่สำหรับสี่ไบต์ newline และตัวปิด กรณีไม่ใช่สี่หลักจะปฏิเสธและจบ ไม่เก็บส่วนต้นของข้อความยาวเป็นรหัสใหม่

C
/* find_record.c : first matching record index, or -1 */
#include <stdio.h>
#include <string.h>
 
struct Student {
    char code[5];
    char name[16];
    int score;
};
 
int find_record(const struct Student room[], int count, const char code[]);
 
int main(void)
{
    const struct Student room[3] = {
        {"0017", "Sample-A", 70},
        {"0018", "Sample-B", 90},
        {"0019", "Sample-C", 80}
    };
    char code[6];
    if (fgets(code, sizeof code, stdin) == NULL) {
        puts("Input error");
        return 1;
    }
    code[strcspn(code, "\n")] = '\0';
    if (strlen(code) != 4) {
        puts("Invalid code");
        return 1;
    }
    for (int i = 0; i < 4; ++i) {
        if (code[i] < '0' || code[i] > '9') {
            puts("Invalid code");
            return 1;
        }
    }
    int index = find_record(room, 3, code);
    if (index == -1) {
        puts("Not found");
    } else {
        printf("index=%d %s %s %d\n", index,
               room[index].code, room[index].name, room[index].score);
    }
    return 0;
}
 
int find_record(const struct Student room[], int count, const char code[])
{
    for (int i = 0; i < count; ++i) {
        if (strcmp(room[i].code, code) == 0) {
            return i;
        }
    }
    return -1;
}

ทดลอง 0017, 0019 และ 9999 พร้อมกรณีข้อความว่างและรหัสห้าหลัก ก่อนรันให้ทำนายทั้งดัชนี ชื่อ และคะแนน กรณีไม่พบต้องไม่ใช้ -1 ไปอ่าน room[-1]

สัญญา find_record กำหนด count ไม่เกินจำนวนระเบียนจริง และคืนตำแหน่งแรกเมื่อมีรหัสตรงหลายตัว ตัวฟังก์ชันค้นหาไม่ได้ทำหน้าที่ตรวจว่าฐานข้อมูลควรยอมรับรหัสซ้ำหรือไม่

9.7 เวิร์กช็อปขยาย: เปรียบเทียบแบบจำลองและพิสูจน์ว่าข้อมูลไม่สลับ

สร้างข้อมูลตัวอย่างสามรายการในแบบอาร์เรย์ขนานและแบบอาร์เรย์ของ struct จากนั้นจำลองการสลับรายการแรกกับสุดท้าย ในแบบแรกต้องสลับทุกอาร์เรย์ที่เกี่ยวข้อง ส่วนแบบระเบียนใช้ตัวแปรชั่วคราวชนิดเดียวกันสลับทั้งรายการ วาดตารางก่อนและหลังพร้อมตรวจว่ารหัสกับคะแนนยังจับคู่เดิม

เพิ่มฟังก์ชันแสดงระเบียนหนึ่งรายการที่รับ struct Student เป็นพารามิเตอร์ และฟังก์ชันคำนวณผลรวมของอาร์เรย์ระเบียนที่รับจำนวนสมาชิกด้วย อธิบายความต่างระหว่างการส่งระเบียนเดี่ยวแบบค่ากับการส่งอาร์เรย์ ไม่สรุปว่าเมื่ออยู่ในฟังก์ชันแล้วการแก้ข้อมูลทุกชนิดมีผลเหมือนกัน

ต่อยอดด้วยการค้นหารหัสสมมติ โดยใช้ strcmp และแสดงรายการที่ตรง ทดสอบกรณีพบตัวแรก ตัวสุดท้าย ไม่พบ และชุดว่าง กรณีไม่พบต้องไม่ใช้ดัชนีที่เป็นสัญญาณผิดพลาดไปอ่านสมาชิกต่อโดยไม่ตรวจ

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

9.8 ตัวอย่างเปรียบเทียบ: ปรับข้อมูลชั่วคราวก่อนยืนยันรายการ

สมมติต้องเพิ่มอุปกรณ์รหัส 0017 ชื่อ Cable จำนวน 17 เส้น และขนาดชุด 3 เส้น หากเขียนรหัสลงอาร์เรย์หลักก่อน แล้วเพิ่งพบว่าขนาดชุดเป็นศูนย์ จะมีรายการที่กรอกเพียงบางช่องหลงอยู่ ต้องตัดสินว่าจะลบคืนอย่างไร การเตรียมระเบียนชั่วคราวช่วยลดงานย้อนกลับ

ลำดับที่ตรวจง่ายคือรับลงระเบียนชั่วคราว ตรวจความยาวรหัสและชื่อ ตรวจจำนวนไม่ติดลบ ตรวจขนาดชุดบวก ตรวจรหัสไม่ซ้ำ และตรวจพื้นที่ว่าง เมื่อผ่านทุกข้อจึงคัดลอกทั้งระเบียนเข้าอาร์เรย์หลักแล้วเพิ่มจำนวนรายการ นี่คือข้อตกลงว่าการเพิ่มหนึ่งรายการสำเร็จครบหรือไม่เปลี่ยนรายการเดิม

ขั้นcount ก่อนเปลี่ยนอาร์เรย์หลักหรือยังกรณีไม่ผ่าน
รับระเบียนชั่วคราว3ยังแจ้งข้อมูลขาด
ตรวจข้อมูลและรหัสซ้ำ3ยังปฏิเสธรายการ
ตรวจความจุ3ยังแจ้งพื้นที่เต็ม
เก็บและเพิ่มจำนวน3 → 4เปลี่ยนหนึ่งตำแหน่งทำเมื่อผ่านครบแล้ว

ต้องตรวจความจุก่อนใช้ items[count] ไม่ใช้ตำแหน่งนั้นเป็นที่รับชั่วคราวก่อนรู้ว่า count ยังน้อยกว่าความจุ การแยกตัวแปรชั่วคราวทำให้เห็นจุดเปลี่ยนสถานะชัด และช่วยออกแบบกรณีทดสอบที่ตรวจข้อมูลเดิมหลังปฏิเสธได้

ลองกำหนดให้รายการใหม่รหัสซ้ำกับรายการแรกแต่มีจำนวนต่างกัน โปรแกรมควรทำอะไร ถ้าโจทย์กำหนดห้ามซ้ำต้องปฏิเสธ ไม่ตีความว่าเป็นการแก้ไขรายการเดิมโดยอัตโนมัติ ถ้าอยากรองรับการแก้ไข ต้องเป็นความสามารถที่มีข้อกำหนดและหลักฐานทดสอบแยก

เวิร์กช็อปนี้ให้ส่งตารางสถานะก่อนและหลังสามกรณี ได้แก่เพิ่มสำเร็จ รหัสซ้ำ และพื้นที่เต็ม เป้าหมายไม่ใช่เพียงได้ข้อความแจ้ง แต่ยืนยันได้ว่าข้อมูลเดิมไม่เปลี่ยนเมื่อคำขอถูกปฏิเสธ ซึ่งจะใช้ตรง ๆ ในโครงงานท้ายรายวิชา

ตัวอย่างตรวจรับ: ผ่านครบก่อนเก็บระเบียนใหม่

สร้างไฟล์ add_record.c เลือกกรณีทดสอบหมายเลข 1–6 ซึ่งเตรียมระเบียนสมมติไว้ในโปรแกรม เพื่อให้ตรวจตรรกะการเพิ่มโดยยังไม่ต้องเขียนแบบฟอร์มรับชื่อหลายช่อง

C
/* add_record.c : validate a temporary record before committing it */
#include <stdio.h>
#include <string.h>
 
struct Student {
    char code[5];
    char name[16];
    int score;
};
 
int add_record(struct Student room[], int count, int capacity,
               struct Student candidate);
 
int main(void)
{
    struct Student room[3] = {
        {"0017", "Sample-A", 70},
        {"0018", "Sample-B", 90}
    };
    const struct Student candidates[6] = {
        {"0019", "Sample-C", 80},
        {"0017", "Other", 50},
        {"0019", "Sample-C", 101},
        {"019", "Sample-C", 80},
        {"0019", "", 80},
        {"0020", "Sample-D", 60}
    };
    int choice;
    if (scanf("%d", &choice) != 1) {
        puts("Input error");
        return 1;
    }
    if (choice < 1 || choice > 6) {
        puts("Range error");
        return 1;
    }
    int count = 2;
    if (choice == 6) {
        room[2] = candidates[0];
        count = 3;
    }
    int next = add_record(room, count, 3, candidates[choice - 1]);
    if (next >= 0) {
        count = next;
        puts("Added");
    } else if (next == -1) {
        puts("Invalid record");
    } else if (next == -2) {
        puts("Duplicate code");
    } else {
        puts("Full");
    }
    printf("count=%d\n", count);
    for (int i = 0; i < count; ++i) {
        printf("%s %s %d\n", room[i].code, room[i].name, room[i].score);
    }
    return 0;
}
 
/* Strings must be terminated within their fields.
   Returns new count, -1 invalid, -2 duplicate, or -3 full.
   Caller guarantees 0 <= count <= capacity <= actual storage. */
int add_record(struct Student room[], int count, int capacity,
               struct Student candidate)
{
    if (strlen(candidate.code) != 4 || candidate.name[0] == '\0' ||
        candidate.score < 0 || candidate.score > 100) {
        return -1;
    }
    for (int i = 0; i < 4; ++i) {
        if (candidate.code[i] < '0' || candidate.code[i] > '9') {
            return -1;
        }
    }
    for (int i = 0; i < count; ++i) {
        if (strcmp(room[i].code, candidate.code) == 0) {
            return -2;
        }
    }
    if (count == capacity) {
        return -3;
    }
    room[count] = candidate;
    return count + 1;
}
หมายเลขกรณีที่เตรียมไว้count ก่อนสถานะที่ทำนายcount หลัง
1ระเบียนใหม่ถูกต้อง2
2รหัสซ้ำกับรายการแรก2
3คะแนนเกินช่วง2
4รหัสยาวเพียงสามหลัก2
5ชื่อว่าง2
6ข้อมูลถูกแต่พื้นที่เต็ม3

ตัวอย่างกำหนดรหัสเป็นเลขสี่หลัก ชื่อไม่ว่าง คะแนน 0–100 และไม่ยอมรับรหัสซ้ำ กติกาเหล่านี้เป็นแบบฝึก ไม่ใช่กฎข้อมูลของทุกระบบ โปรแกรมนี้เลือกกรณีสำเร็จรูป ไม่ใช่แบบฟอร์มรับระเบียนจากผู้ใช้ครบทุกช่อง

ลำดับตรวจคือข้อมูล → รหัสซ้ำ → ความจุ → เก็บ หากรายการทั้งซ้ำและเต็มจะรายงานซ้ำตามลำดับนี้ ทุกเส้นทางปฏิเสธต้องไม่เปลี่ยนระเบียนที่มีอยู่ ผู้เรียกต้องส่ง count และความจุให้ตรงพื้นที่จริงเช่นเดียวกับบทอาร์เรย์

ชื่อที่มีแต่ช่องว่างยังถือว่าไม่ว่างตามกติกาอย่างง่ายนี้ หากระบบต้องปฏิเสธชื่อแบบนั้น ต้องเพิ่มกติกาตรวจข้อความและกรณีทดสอบ ไม่ตีความว่าเงื่อนไขตัวแรกไม่เป็นศูนย์ตรวจคุณภาพชื่อได้ครบแล้ว

ใบงานสถานะ: หลักฐานว่าปฏิเสธแล้วข้อมูลเดิมยังอยู่

คัดตารางรหัส ชื่อ และคะแนนของ room ก่อนเรียก add_record แล้วเปรียบเทียบหลังเรียกหมายเลข 2, 3 และ 6 ทีละกรณี อย่าตรวจเพียงข้อความแจ้งหรือ count

สิ่งที่ตรวจก่อนเรียกหลังเรียกเมื่อรหัสซ้ำหลังเรียกเมื่อคะแนนผิด
count
room[0] ทั้งสามสมาชิก
room[1] ทั้งสามสมาชิก
มีรายการใหม่ให้ประมวลผลหรือไม่

ระบุจุดเดียวที่เริ่มแก้พื้นที่หลักในฟังก์ชัน หากย้ายคำสั่ง room[count] = candidate ไปก่อนตรวจความจุ จะทำให้เส้นทางปฏิเสธเต็มไม่ปลอดภัย ให้ชี้เหตุผลจากดัชนี ไม่รันการเขียนเกินเพื่อพิสูจน์

เทียบการส่ง candidate หนึ่งระเบียนแบบ by value กับการส่ง room ซึ่งเข้าถึงสมาชิกอาร์เรย์เดิมได้ การอยู่ในฟังก์ชันเหมือนกันไม่ได้ทำให้รูปแบบการเปลี่ยนข้อมูลเหมือนกัน

ใบงานออกแบบ: เปลี่ยนจากระเบียนคะแนนเป็นระเบียนอุปกรณ์

ใช้ตารางออกแบบสมาชิกจากใบงาน 1 แล้วเขียนสัญญาฟังก์ชันสำหรับหาจำนวนชุดเต็มจากระเบียนอุปกรณ์ กำหนดจำนวนคงเหลือช่วง 0–1000 และจำนวนต่อชุดช่วง 1–100 สำหรับการฝึก

ข้อมูลอุปกรณ์สมมติจำนวนชุดที่ทำนายของเหลือที่ทำนายสมาชิกใดใช้ระบุตัวตน
0017 Cable จำนวน 17 ชุดละ 3
0018 Cable จำนวน 18 ชุดละ 3
0019 Adapter จำนวน 0 ชุดละ 2
0020 Cable จำนวน 17 ชุดละ 0

ชื่อ Cable ที่เหมือนกันไม่ได้เป็นเหตุว่ารหัสซ้ำ และจำนวน 0 เป็นข้อมูลปกติ ต่างจากจำนวนต่อชุด 0 ที่ห้ามใช้เป็นตัวหาร เขียนข้อกำหนดเหล่านี้แยกจากการนิยามชนิดให้ชัด

9.9 อ่านโค้ดที่ผิดและทำความเข้าใจข้อความวินิจฉัย

ตัวอย่างต่อไปนี้เป็นโค้ดฝึกแยกจากงานที่ต้องส่ง แสดงข้อผิดพลาดที่สัมพันธ์กับเนื้อหาบท พร้อมข้อความวินิจฉัยจริง วิธีอ่าน และฉบับแก้ คำว่า diagnostic ใช้รวมข้อความ error, warning และ note จึงไม่ควรเรียกทุกบรรทัดว่า error หรือถือว่าการไม่มี error แปลว่าโปรแกรมถูกตามโจทย์

ที่มาของ log: ผู้เขียนทดสอบด้วย GCC 11.4.0 บน Ubuntu ผ่าน Linux environment เมื่อ 24 ตุลาคม 2569 ใช้ C11 และตัวเลือกดังนี้ ชื่อไฟล์ เลขบรรทัด คอลัมน์ ถ้อยคำ และคำเตือนที่เปิดใช้อาจต่างจากแพลตฟอร์มการเรียน นักศึกษาไม่จำเป็นต้องเปลี่ยนเครื่องมือหรือการติดตั้งตามชุดทดสอบของผู้เขียน

TEXT
-std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0

ข้อความรูปแบบ bad.c:5:17 อ่านว่าไฟล์ bad.c บรรทัด 5 คอลัมน์ 17 ส่วน ^ และ ~ ช่วยชี้บริเวณที่ตรวจพบ จุดนี้อาจเป็นปลายเหตุจากบรรทัดก่อนหน้า error ทำให้การสร้างโปรแกรมล้มในกรณีนั้น warning เตือนความเสี่ยงแม้คำสั่งสร้างอาจจบสำเร็จ และ note ให้บริบท เช่น declaration เดิม ต้องอ่านคู่กัน (GCC: Diagnostics)

ตัวอย่างที่ผิด 1: ใช้ชื่อสมาชิก struct ที่ไม่มีอยู่

ประเภท: ข้อผิดพลาดขณะคอมไพล์

struct ประกาศสมาชิก score แต่โค้ดเรียก scores การมีตัวแปรชื่ออื่นคล้ายกันหรือการเพิ่มคะแนนหลายรายการไม่ได้ทำให้สมาชิกใหม่เกิดขึ้นเอง ต้องตรวจนิยามระเบียนที่ใช้จริง

โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้

C
#include <stdio.h>
struct Student {
    int id;
    double score;
};
int main(void)
{
    struct Student student = {1, 85.0};
    printf("%.1f\n", student.scores);
    return 0;
}

คำสั่งสร้างและข้อความที่ได้จริง

TEXT
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad
TEXT
bad.c: In function 'main':
bad.c:9:30: error: 'struct Student' has no member named 'scores'; did you mean 'score'?
    9 |     printf("%.1f\n", student.scores);
      |                              ^~~~~~
      |                              score
bad.c:8:20: warning: variable 'student' set but not used [-Wunused-but-set-variable]
    8 |     struct Student student = {1, 85.0};
      |                    ^~~~~~~

สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน

อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร

has no member named ชี้ว่าชนิด struct นี้ไม่มีสมาชิกตามชื่อหลังจุด did you mean ถ้าปรากฏเป็นข้อเสนอให้ตรวจ ไม่ใช่คำสั่งให้เพิ่มสมาชิกใหม่โดยไม่ดูแบบจำลอง

โค้ดที่แก้แล้ว

C
#include <stdio.h>
struct Student {
    int id;
    double score;
};
int main(void)
{
    struct Student student = {1, 85.0};
    printf("%.1f\n", student.score);
    return 0;
}

เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ

ใช้ student.score ให้ตรงกับนิยามและคงชนิด double การเปลี่ยนเป็น student.id เพียงเพื่อให้ compiler ผ่านจะเปลี่ยนความหมายของข้อมูลที่รายงาน

ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง

TEXT
85.0

ตัวอย่างที่ผิด 2: แก้สำเนาระเบียนแต่คาดว่าต้นฉบับเปลี่ยน

ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน

ฟังก์ชันรับ struct แบบ by value แล้วคืนระเบียนที่ปรับแล้ว แต่ main ทิ้งค่าคืน ต้นฉบับจึงยังเป็น 70.0 แม้คำสั่งในฟังก์ชันทำงานครบ การไม่มี warning ไม่ได้พิสูจน์ว่าเกิดการอัปเดตตามที่ต้องการ

โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้

C
#include <stdio.h>
struct Student { double score; };
struct Student add_bonus(struct Student student)
{
    student.score += 5.0;
    return student;
}
int main(void)
{
    struct Student student = {70.0};
    add_bonus(student);
    printf("score=%.1f\n", student.score);
    return 0;
}

คำสั่งสร้างและข้อความที่ได้จริง

TEXT
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad

ไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม

สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น

ผลจากการรันฉบับผิด

stdout:

TEXT
score=70.0

stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน

สถานะจบที่ตัวเก็บผลอ่านได้: 0

ผลตามข้อกำหนดที่ใช้เทียบ:

TEXT
score=75.0

อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร

ไม่มี compiler diagnostic สำหรับการเรียกที่ถูกชนิดนี้ ให้ trace ระเบียนของ main สำเนาพารามิเตอร์ และค่าที่ return เป็นสามช่วง แยกสิ่งที่ฟังก์ชันเปลี่ยนออกจากสิ่งที่ผู้เรียกรับกลับ

โค้ดที่แก้แล้ว

C
#include <stdio.h>
struct Student { double score; };
struct Student add_bonus(struct Student student)
{
    student.score += 5.0;
    return student;
}
int main(void)
{
    struct Student student = {70.0};
    student = add_bonus(student);
    printf("score=%.1f\n", student.score);
    return 0;
}

เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ

รับค่าคืนกลับเข้า student ตามตัวอย่าง ใช้วิธีนี้ได้โดยไม่เพิ่ม pointer นอกขอบเขตบท และทดสอบทั้งก่อนเรียก หลังเรียก และการเรียกซ้ำเพื่อดูผลการอัปเดต

ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง

TEXT
score=75.0

ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้

  1. ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
  2. อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
  3. แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
  4. ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน

ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)

ปฏิบัติการที่ 9 โครงสร้างข้อมูลแบบระเบียน

ปฏิบัติการนี้แบ่งเป็นสองส่วนที่ต่อเนื่องกันในไฟล์เดียว ส่วนแรกฝึกนิยามระเบียนเดี่ยวและแสดงผลด้วยฟังก์ชัน ส่วนที่สองต่อยอดด้วยอาร์เรย์ของระเบียนและการคำนวณค่าเฉลี่ยของเกรด ควรทำตามลำดับเพราะส่วนที่สองต่อยอดจากส่วนแรก

ℹ️Note

ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0

Lab A: นิยามระเบียนและฟังก์ชันแสดงผล

สถานการณ์ ต้องการสร้างระเบียนนักศึกษาหนึ่งคนที่มีชื่อ รหัส และเกรดเฉลี่ย แล้วแสดงข้อมูลด้วยฟังก์ชันที่รับระเบียน เพื่อฝึกการนิยาม struct การเข้าถึงสมาชิกด้วยตัวดำเนินการจุด และการส่งระเบียนเข้าฟังก์ชัน

เป้าหมาย ฝึก LLO9.1 และ LLO9.3 คือนิยาม struct และเข้าถึงสมาชิกด้วยจุด และส่งระเบียนเข้าฟังก์ชันเพื่อแยกงานการแสดงผล

ขั้นปฏิบัติ

  1. สร้างไฟล์ชื่อ ch9_struct.c ด้วยโปรแกรมแก้ไขข้อความหรือ IDE
  2. พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
C
/* ch9_struct.c : โครงสร้างข้อมูลแบบระเบียน การเข้าถึงสมาชิก และการส่งเข้าฟังก์ชัน */
#include <stdio.h>
#include <string.h>
 
struct Student {
    char name[20];
    long id;
    double gpa;
};
 
void print_student(struct Student s) {
    printf("รหัส %ld: %s (GPA %.2f)\n", s.id, s.name, s.gpa);
}
 
int main(void) {
    struct Student a;
    strcpy(a.name, "Somchai");
    a.id = 65001L;
    a.gpa = 3.75;
    print_student(a);
    return 0;
}
  1. คอมไพล์และรันด้วยคำสั่ง
BASH
gcc -std=c11 -Wall ch9_struct.c -o ch9_struct
./ch9_struct

ผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)

TEXT
รหัส 65001: Somchai (GPA 3.75)

นิยาม struct Student รวมสมาชิกสามตัวต่างชนิดกันไว้ในระเบียนเดียว การกำหนดค่าชื่อใช้ strcpy(a.name, "Somchai"); เพราะชื่อเป็นอาร์เรย์ของอักขระ ส่วนรหัสและเกรดเฉลี่ยกำหนดด้วยเครื่องหมายเท่ากับผ่านตัวดำเนินการจุด ฟังก์ชัน print_student รับระเบียนแบบส่งสำเนาค่าแล้วแสดงสมาชิกด้วยตัวดำเนินการจุด จึงแยกงานการแสดงผลออกจาก main

ตัดสินใจเอง A.1 ต่อยอดระเบียนโดยเพิ่มสมาชิก year ที่เป็นชั้นปีของนักศึกษา กำหนดค่าให้ year ด้วยตัวดำเนินการจุด แล้วปรับฟังก์ชัน print_student ให้แสดงชั้นปีเพิ่ม พร้อมตั้งชื่อสมาชิกและปรับข้อความแสดงผลให้สื่อความหมาย

ผลลัพธ์ที่คาดหวัง A.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และแสดงชั้นปีเพิ่มได้ถูกต้องตามค่าที่กำหนด (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)

จุดตรวจ A นิยาม struct ถูกต้องและปิดท้ายด้วยอัฒภาค เข้าถึงสมาชิกด้วยตัวดำเนินการจุดถูก กำหนดค่าชื่อด้วย strcpy ฟังก์ชันแสดงผลทำงานและแยกออกจาก main และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
คอมไพล์ไม่ผ่านที่บรรทัดหลังนิยาม structลืมเครื่องหมายอัฒภาคท้ายนิยาม structปิดท้ายนิยาม struct ด้วยอัฒภาคเสมอ
คอมไพล์ไม่ผ่านเมื่อกำหนดค่าชื่อกำหนดชื่อด้วยเครื่องหมายเท่ากับ เช่น a.name = "Somchai";ใช้ strcpy(a.name, "Somchai"); และรวม string.h
ชื่อแสดงเป็นค่าขยะใช้สมาชิกชื่อก่อนกำหนดค่ากำหนดค่าชื่อด้วย strcpy ก่อนนำไปแสดง

เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว

ใบงานตรวจ Lab A: ตั้งสมาชิกครบก่อนแสดง

การตรวจสิ่งที่ต้องบันทึก
name มีตัวปิดและพื้นที่พอความจุ 20 และความยาวชื่อสมมติที่ใช้
id ใช้ long กับ %ldค่าที่กำหนดกับผลที่แสดง
gpa ใช้ double กับ %.2fค่าเข้าและจำนวนตำแหน่งที่แสดง
print_student รับหนึ่งระเบียนอาร์กิวเมนต์และสมาชิกที่พิมพ์
เพิ่ม year ใน A.1จุดเพิ่มในนิยาม การตั้งค่า และรูปแบบแสดง

หากเพิ่ม year ต้องตั้งค่าก่อนอ่านหรือแสดงในทุกระเบียนที่นำมาใช้ การเพิ่มสมาชิกในนิยามอย่างเดียวไม่ทำให้ข้อมูล local ที่ยังไม่ตั้งค่ามีชั้นปีที่ถูกต้อง

ลองคัดลอกระเบียนแล้วเปลี่ยนชื่อในสำเนา พร้อมแสดงต้นฉบับคู่กัน ใช้ชื่อสั้นที่พอดีบัฟเฟอร์ ไม่เพิ่มโจทย์ด้วยการปล่อยให้ strcpy เขียนเกินพื้นที่

Lab B: อาร์เรย์ของระเบียนและค่าเฉลี่ย

สถานการณ์ ต้องการเก็บนักศึกษาหลายคนไว้ในอาร์เรย์ของระเบียน แล้ววนแสดงข้อมูลและคำนวณเกรดเฉลี่ยของทั้งห้อง เพื่อฝึกการใช้อาร์เรย์ของระเบียนและการสรุปข้อมูล

เป้าหมาย ฝึก LLO9.2 และ LLO9.3 คือใช้อาร์เรย์ของระเบียนและวนประมวลผลสมาชิก และส่งระเบียนแต่ละตัวเข้าฟังก์ชันแสดงผล

ขั้นปฏิบัติ

  1. ต่อยอดไฟล์ ch9_struct.c จาก Lab A โดยเพิ่มอาร์เรย์ของระเบียนและการวนคำนวณค่าเฉลี่ยใน main ให้ซอร์สโค้ดเป็นดังนี้
C
/* ch9_struct.c : โครงสร้างข้อมูลแบบระเบียน การเข้าถึงสมาชิก และการส่งเข้าฟังก์ชัน */
#include <stdio.h>
#include <string.h>
 
struct Student {
    char name[20];
    long id;
    double gpa;
};
 
void print_student(struct Student s) {
    printf("รหัส %ld: %s (GPA %.2f)\n", s.id, s.name, s.gpa);
}
 
int main(void) {
    struct Student a;
    strcpy(a.name, "Somchai");
    a.id = 65001L;
    a.gpa = 3.75;
    print_student(a);
 
    struct Student room[2] = {
        {"Anong", 65002L, 3.90},
        {"Decha", 65003L, 2.85}
    };
    double sum = 0;
    for (int i = 0; i < 2; i++) {
        print_student(room[i]);
        sum += room[i].gpa;
    }
    printf("GPA เฉลี่ยของห้อง = %.2f\n", sum / 2);
    return 0;
}
  1. คอมไพล์และรันด้วยคำสั่ง
BASH
gcc -std=c11 -Wall ch9_struct.c -o ch9_struct
./ch9_struct

ผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)

TEXT
รหัส 65001: Somchai (GPA 3.75)
รหัส 65002: Anong (GPA 3.90)
รหัส 65003: Decha (GPA 2.85)
GPA เฉลี่ยของห้อง = 3.38

อาร์เรย์ room เก็บระเบียนนักศึกษาสองคน กำหนดค่าเริ่มต้นด้วยวงเล็บปีกกาซ้อน การวนด้วยลูป for เข้าถึงระเบียนตัวที่ i แล้วส่งเข้าฟังก์ชัน print_student(room[i]) และสะสมเกรดด้วย sum += room[i].gpa; ค่าเฉลี่ยคำนวณจาก sum / 2 และแสดงด้วย %.2f ได้ 3.38 ซึ่งเป็นการปัดจาก 3.375 ตามการแสดงผลทศนิยมสองตำแหน่ง เงื่อนไขลูป i < 2 ทำให้วนถึงดัชนี 0 และ 1 พอดีโดยไม่เกินขอบเขต

ตัดสินใจเอง B.1 ต่อยอดโปรแกรมโดยเพิ่มการหานักศึกษาที่มีเกรดเฉลี่ยสูงสุดในอาร์เรย์ โดยตั้งค่าเริ่มต้นเป็นระเบียนตัวแรก แล้ววนเทียบ room[i].gpa ทีละตัว ปรับระเบียนที่มีเกรดสูงสุดเมื่อพบตัวที่มากกว่า แล้วแสดงผลด้วยฟังก์ชัน print_student พร้อมตั้งชื่อตัวแปรให้สื่อความหมาย

ผลลัพธ์ที่คาดหวัง B.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และแสดงนักศึกษาที่มีเกรดเฉลี่ยสูงสุดได้ถูกต้อง เช่น จากอาร์เรย์ตัวอย่างควรได้ Anong ที่มีเกรด 3.90 (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)

จุดตรวจ B สร้างอาร์เรย์ของระเบียนและกำหนดค่าเริ่มต้นถูก วนประมวลผลด้วยลูปอยู่ในช่วงดัชนี 0 ถึง n-1 ไม่เกินขอบเขต ค่าเฉลี่ยเกรดถูกต้องและแสดงด้วย %.2f ส่งระเบียนแต่ละตัวเข้าฟังก์ชันถูก และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
โปรแกรมแสดงค่าแปลกหรือพังวนเกินขอบเขตด้วยเงื่อนไข i <= 2 จึงเข้าถึง room[2]ใช้เงื่อนไข i < 2 ให้ตัวนับน้อยกว่าจำนวนสมาชิก
ค่าเฉลี่ยกลายเป็นจำนวนเต็มหรือคลาดเคลื่อนใช้ตัวแปรสะสมเป็นจำนวนเต็ม หรือไม่ใช้ %.2fใช้ double กับตัวสะสมและแสดงด้วย %.2f
เข้าถึงสมาชิกผิดเขียน room.gpa[i] แทน room[i].gpaใช้ดัชนีเลือกระเบียนก่อนแล้วจึงตามด้วยจุดและชื่อสมาชิก

เปิดหลังผู้เรียนส่งงานแล้ว

ใบงานตรวจ Lab B: นับเฉพาะระเบียนใน room

Lab B แสดง a ก่อน แล้วจึงแสดง room สองรายการ ผลเฉลี่ยของ room คิดเฉพาะสองรายการใน room ไม่รวม a เพียงเพราะปรากฏบนหน้าจอเดียวกัน

gpa สองรายการใน roomผลรวมที่ทำนายเฉลี่ยที่ทำนายรายการที่ควรได้เมื่อหาค่าสูงสุด
3.90 และ 2.85
0.00 และ 0.00
2.50 และ 2.50
1.25 และ 4.00

สำหรับ B.1 กำหนดว่าถ้า GPA เท่ากันเลือกตัวแรก ใช้เงื่อนไขมากกว่าโดยไม่เท่ากับ และอ่านชื่อกับ GPA จากระเบียนที่เลือกเดียวกัน ชุดว่างต้องรายงานไม่มีข้อมูลก่อนตั้งระเบียนที่ดีที่สุดจากสมาชิกแรก

การลดจำนวนข้อมูลจริงไม่เท่ากับลดความจุ ต้องใช้ count ที่ตรงกับรายการที่มีความหมายและตั้งค่าครบ ไม่วนอ่านสมาชิกที่ไม่ได้ใช้เพียงเพราะยังอยู่ในพื้นที่

เก็บกวาด

เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น ch9_struct) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด

ตรวจความเข้าใจก่อนเริ่มบทที่ 10

ตอบด้วยสมาชิก พิกัด หรือเส้นทางเปลี่ยนสถานะจากตัวอย่าง ไม่ใช้เพียงการท่องคำว่า struct

  1. การนิยามชนิดกับการประกาศตัวแปรหนึ่งระเบียนต่างกันอย่างไร
  2. เหตุใด room[i].score จึงใช้ดัชนีก่อนจุด
  3. ทำไมคัดลอกทั้งระเบียนได้ แต่กำหนดค่าอาร์เรย์ชื่อทั้งก้อนด้วย = ภายหลังไม่ได้
  4. การแก้สมาชิกใน with_bonus จะเปลี่ยน original เมื่อใด
  5. ถ้าสลับเฉพาะคะแนน ข้อมูลใดอาจไม่ตรงกับความจริงของรายการ
  6. เมื่อค้นหาไม่พบ ต้องตรวจอะไรจึงจะไม่อ่านดัชนี -1
  7. การเพิ่มรายการควรเริ่มเปลี่ยนพื้นที่หลักหลังตรวจเงื่อนไขใดบ้าง

เมื่ออธิบายได้ ให้นำตารางรหัส ชื่อ และคะแนนไปใช้เป็นหน่วยข้อมูลที่จะเคลื่อนย้ายร่วมกันใน บทที่ 10 การเรียงลำดับและการค้นหา

สรุป

บทนี้นำอาร์เรย์จากบทที่ 7 ฟังก์ชันจากบทที่ 6 และสตริงจากบทที่ 8 มาประกอบกันเป็นโครงสร้างข้อมูลแบบระเบียน (struct) ที่รวมข้อมูลต่างชนิดของสิ่งเดียวกันไว้ในระเบียนเดียว เริ่มจากการนิยาม struct ด้วย struct Student { char name[20]; int id; double gpa; }; และการประกาศตัวแปรระเบียน ต่อด้วยการเข้าถึงสมาชิกด้วยตัวดำเนินการจุด เช่น a.id และ a.gpa โดยกำหนดค่าชื่อที่เป็นสตริงด้วย strcpy จากบทที่ 8 จากนั้นเป็นอาร์เรย์ของระเบียน struct Student room[2] ที่เข้าถึงสมาชิกด้วย room[i].gpa และวนด้วยลูป for เพื่อสรุปข้อมูล เช่น ค่าเฉลี่ยของเกรด สุดท้ายคือการส่งระเบียนเข้าฟังก์ชันด้วย void print_student(struct Student s) แบบส่งสำเนาค่า ซึ่งการแก้สมาชิกภายในฟังก์ชันไม่กระทบระเบียนเดิม ต่างจากการส่งอาร์เรย์เข้าฟังก์ชันในบทที่ 7 ทักษะเหล่านี้เป็นพื้นฐานของการจัดการข้อมูลที่เป็นระเบียนในบทถัดไป

สิ่งที่ส่งต่อไปยังบทถัดไป

สิ่งที่ส่งต่อค่า/รูปแบบใช้ในบท
การนิยาม struct และการเข้าถึงสมาชิกด้วยจุดstruct Student { ... };, a.id, a.gpaบท 10 (เรียง/ค้นหา), บท 15 (โครงงาน)
อาร์เรย์ของระเบียนและการวนประมวลผลstruct Student room[n];, room[i].gpaบท 10 (เรียง/ค้นหา), บท 15 (โครงงาน)
การส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่าvoid print_student(struct Student s)บท 10 เป็นต้นไป

สรุปคำสั่ง

คำสั่งหน้าที่
gcc -std=c11 -Wall ch9_struct.c -o ch9_structคอมไพล์ซอร์สโค้ด ch9_struct.c เป็นไฟล์ปฏิบัติการ ch9_struct พร้อมเปิดการแจ้งเตือน
./ch9_structรันไฟล์ปฏิบัติการที่คอมไพล์แล้ว

แบบฝึกหัดท้ายบท

ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)

ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)

กลุ่ม ก (เข้าใจ)

  • ก1 (LLO9.1) จงอธิบายว่าโครงสร้างข้อมูลแบบระเบียน (struct) ต่างจากอาร์เรย์อย่างไรในแง่ชนิดข้อมูลของสมาชิก และยกตัวอย่างข้อมูลที่เหมาะกับการเก็บเป็นระเบียน
  • ก2 (LLO9.3) จงอธิบายว่าเมื่อส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่า การแก้สมาชิกภายในฟังก์ชันกระทบระเบียนเดิมหรือไม่ เพราะเหตุใด

กลุ่ม ข (ประยุกต์)

  • ข1 (LLO9.1) จงเขียนโปรแกรมภาษา C ที่นิยาม struct สำหรับหนังสือหนึ่งเล่ม ประกอบด้วยชื่อเรื่อง (สตริง) จำนวนหน้า (จำนวนเต็ม) และราคา (ทศนิยม) กำหนดค่าให้สมาชิกทุกตัวด้วยตัวดำเนินการจุดและ strcpy แล้วแสดงออกทางหน้าจอ จากนั้นคอมไพล์และรันให้ได้ผลจริง
  • ข2 (LLO9.2) จงเขียนโปรแกรมที่สร้างอาร์เรย์ของระเบียนหนังสือ 3 เล่มพร้อมกำหนดค่าเริ่มต้น แล้วใช้ลูปหาราคาเฉลี่ยของทั้งสามเล่ม แสดงราคาเฉลี่ยเป็นทศนิยมสองตำแหน่งโดยไม่เกินขอบเขตดัชนี

กลุ่ม ค (วิเคราะห์)

  • ค1 (LLO9.3) จากโปรแกรมที่ส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่าแล้วพยายามแก้สมาชิกภายในฟังก์ชันเพื่อหวังให้ค่าที่ main เปลี่ยนตาม จงวิเคราะห์ว่าเหตุใดค่าที่ main จึงไม่เปลี่ยน และอธิบายว่าต่างจากการส่งอาร์เรย์เข้าฟังก์ชันในบทที่ 7 อย่างไร

การศึกษาค้นคว้าเพิ่มเติม

  1. ศึกษาโครงสร้างข้อมูลแบบระเบียนและการเข้าถึงสมาชิกเพิ่มเติมจาก Kernighan และ Ritchie (1988) บทที่ 6 ว่าด้วยโครงสร้าง (structures) แล้วทดลองเขียนโปรแกรมที่นิยามระเบียนของตนเองและแสดงสมาชิก
  2. ทบทวนข้อกำหนดเรื่องชนิดข้อมูลแบบโครงสร้างในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจการจัดวางสมาชิกของระเบียน
  3. ทดลองออกแบบโปรแกรมที่ใช้อาร์เรย์ของระเบียนเก็บข้อมูลหลายรายการ แล้วเพิ่มฟังก์ชันที่รับระเบียนไปแสดงผลหรือสรุปข้อมูล เพื่อฝึกการแยกหน้าที่ของฟังก์ชัน

เอกสารอ้างอิง

International Organization for Standardization. (2011). ISO/IEC 9899:2011 — Information technology — Programming languages — C. ISO.

Kernighan, B. W., & Ritchie, D. M. (1988). The C programming language (2nd ed.). Prentice Hall.

ตำราหลักภาษาไทยของรายวิชา. (ผู้สอนเติมรายละเอียดบรรณานุกรม — เพิ่มตามมติผู้สอน 21 ต.ค. 2569).

แหล่งปฐมภูมิที่ตรวจประกอบการปรับปรุง 23 ตุลาคม 2569

ใช้เอกสารข้างต้นตรวจหลักการก่อนเรียบเรียงส่วนขยายและสร้างข้อมูลสมมติ ตัวอย่างใหม่ที่เป็นโปรแกรม C ครบไฟล์ตรวจด้วย GCC 12.2.0 (Debian Linux) และ GCC 6.3.0 (MinGW-w64), C11 และคำเตือน -Wall -Wextra -Wpedantic เมื่อ 23 ตุลาคม 2569 แยกจากผลทดสอบ GCC 13.3.0 ของตัวอย่างเดิม ค่ารูปแบบและผลคำนวณตรวจจากการรัน ส่วนเวลา CPU ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว

แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 9

  • GNU C: Structure Assignment — การคัดลอกระเบียนและเนื้อหาอาร์เรย์ที่เป็นสมาชิก
  • GNU C: Function Call Semantics — การส่งสำเนาค่าและการนำค่าคืนมาใช้
  • GNU C: Array parameters are pointers — ความต่างของพารามิเตอร์อาร์เรย์กับระเบียนเดี่ยว

ตรวจเอกสาร Structure Assignment เมื่อ 23 ตุลาคม 2569 และใช้หลักการส่งค่ากับพารามิเตอร์อาร์เรย์ที่ตรวจในชุดสอนบท 6–7 ตัวอย่างใช้ hosted C11 และข้อมูลสมมติในขอบเขตที่กำหนด ไม่ใช้ข้อมูลผู้เรียนจริง

By Watcharin Sarachai