Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 9 โครงสร้างข้อมูลแบบระเบียน (struct)
แผนบริหารการสอนประจำบทที่ 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 ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท
วิธีสอน
- ทบทวนอาร์เรย์จากบทที่ 7 ที่เก็บข้อมูลชนิดเดียวกันหลายค่า แล้วตั้งโจทย์ที่ต้องเก็บข้อมูลต่างชนิดของสิ่งเดียวกัน เช่น ชื่อ รหัส และเกรดเฉลี่ยของนักศึกษาหนึ่งคน เพื่อชี้ให้เห็นความจำเป็นของระเบียนแทนการประกาศตัวแปรแยกหลายตัว
- แนะนำการนิยาม struct และการเข้าถึงสมาชิกด้วยตัวดำเนินการจุด โดยสาธิตบนเครื่องจริง และย้ำการกำหนดค่าสตริงด้วย
strcpyจากบทที่ 8 แทนการใช้เครื่องหมายเท่ากับ - สาธิตการสร้างอาร์เรย์ของระเบียนและการวนประมวลผลสมาชิกด้วยลูป
forจากบทที่ 5 เพื่อแสดงและสรุปข้อมูล เช่น ค่าเฉลี่ยของเกรด - อธิบายการส่งระเบียนเข้าฟังก์ชันแบบส่งสำเนาค่า และชี้ว่าการแก้สมาชิกภายในฟังก์ชันไม่กระทบระเบียนเดิม เช่นเดียวกับการส่งตัวแปรเดี่ยวในบทที่ 6
- ให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา 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.1 | CLO1 |
| 2 | สร้างอาร์เรย์ของระเบียนแล้ววนคำนวณค่าเฉลี่ยเกรด | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO9.2 | CLO2 |
| 3 | เขียนฟังก์ชันรับระเบียนแล้วแสดงผล แยกงานการแสดงออกจาก main | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO9.3 | CLO2 |
เงื่อนไขผ่าน: งานข้อ 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 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย 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 ตามด้วยชื่อระเบียนและรายการสมาชิกในวงเล็บปีกกา โดยแต่ละสมาชิกระบุชนิดข้อมูลและชื่อ และต้องปิดท้ายนิยามด้วยเครื่องหมายอัฒภาค ตัวอย่างระเบียนนักศึกษาที่มีชื่อ รหัส และเกรดเฉลี่ย นิยามได้ดังนี้
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 ในการแสดงผล
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
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]);
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 และบทสตริง
#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 ตัวอย่างใช้เฉพาะสมาชิกอาร์เรย์ที่เก็บข้อมูลภายใน ไม่มีสมาชิกพอยน์เตอร์
/* 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 แล้วสลับทั้งระเบียน ใช้ข้อมูลที่คะแนนต่างกันเพื่อให้เห็นว่าคะแนนยังอยู่คู่กับรหัสและชื่อเดิม
/* 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 และตัวปิด กรณีไม่ใช่สี่หลักจะปฏิเสธและจบ ไม่เก็บส่วนต้นของข้อความยาวเป็นรหัสใหม่
/* 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 ซึ่งเตรียมระเบียนสมมติไว้ในโปรแกรม เพื่อให้ตรวจตรรกะการเพิ่มโดยยังไม่ต้องเขียนแบบฟอร์มรับชื่อหลายช่อง
/* 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 และตัวเลือกดังนี้ ชื่อไฟล์ เลขบรรทัด คอลัมน์ ถ้อยคำ และคำเตือนที่เปิดใช้อาจต่างจากแพลตฟอร์มการเรียน นักศึกษาไม่จำเป็นต้องเปลี่ยนเครื่องมือหรือการติดตั้งตามชุดทดสอบของผู้เขียน
-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 การมีตัวแปรชื่ออื่นคล้ายกันหรือการเพิ่มคะแนนหลายรายการไม่ได้ทำให้สมาชิกใหม่เกิดขึ้นเอง ต้องตรวจนิยามระเบียนที่ใช้จริง
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#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;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o badbad.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 ถ้าปรากฏเป็นข้อเสนอให้ตรวจ ไม่ใช่คำสั่งให้เพิ่มสมาชิกใหม่โดยไม่ดูแบบจำลอง
โค้ดที่แก้แล้ว
#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 ว่าง
85.0ตัวอย่างที่ผิด 2: แก้สำเนาระเบียนแต่คาดว่าต้นฉบับเปลี่ยน
ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน
ฟังก์ชันรับ struct แบบ by value แล้วคืนระเบียนที่ปรับแล้ว แต่ main ทิ้งค่าคืน ต้นฉบับจึงยังเป็น 70.0 แม้คำสั่งในฟังก์ชันทำงานครบ การไม่มี warning ไม่ได้พิสูจน์ว่าเกิดการอัปเดตตามที่ต้องการ
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#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;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o badไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม
สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ผลจากการรันฉบับผิด
stdout:
score=70.0stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน
สถานะจบที่ตัวเก็บผลอ่านได้: 0
ผลตามข้อกำหนดที่ใช้เทียบ:
score=75.0อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
ไม่มี compiler diagnostic สำหรับการเรียกที่ถูกชนิดนี้ ให้ trace ระเบียนของ main สำเนาพารามิเตอร์ และค่าที่ return เป็นสามช่วง แยกสิ่งที่ฟังก์ชันเปลี่ยนออกจากสิ่งที่ผู้เรียกรับกลับ
โค้ดที่แก้แล้ว
#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 ว่าง
score=75.0ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 9 โครงสร้างข้อมูลแบบระเบียน
ปฏิบัติการนี้แบ่งเป็นสองส่วนที่ต่อเนื่องกันในไฟล์เดียว ส่วนแรกฝึกนิยามระเบียนเดี่ยวและแสดงผลด้วยฟังก์ชัน ส่วนที่สองต่อยอดด้วยอาร์เรย์ของระเบียนและการคำนวณค่าเฉลี่ยของเกรด ควรทำตามลำดับเพราะส่วนที่สองต่อยอดจากส่วนแรก
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0
Lab A: นิยามระเบียนและฟังก์ชันแสดงผล
สถานการณ์ ต้องการสร้างระเบียนนักศึกษาหนึ่งคนที่มีชื่อ รหัส และเกรดเฉลี่ย แล้วแสดงข้อมูลด้วยฟังก์ชันที่รับระเบียน เพื่อฝึกการนิยาม struct การเข้าถึงสมาชิกด้วยตัวดำเนินการจุด และการส่งระเบียนเข้าฟังก์ชัน
เป้าหมาย ฝึก LLO9.1 และ LLO9.3 คือนิยาม struct และเข้าถึงสมาชิกด้วยจุด และส่งระเบียนเข้าฟังก์ชันเพื่อแยกงานการแสดงผล
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch9_struct.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* 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;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch9_struct.c -o ch9_struct
./ch9_structผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
รหัส 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 คือใช้อาร์เรย์ของระเบียนและวนประมวลผลสมาชิก และส่งระเบียนแต่ละตัวเข้าฟังก์ชันแสดงผล
ขั้นปฏิบัติ
- ต่อยอดไฟล์
ch9_struct.cจาก Lab A โดยเพิ่มอาร์เรย์ของระเบียนและการวนคำนวณค่าเฉลี่ยในmainให้ซอร์สโค้ดเป็นดังนี้
/* 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;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch9_struct.c -o ch9_struct
./ch9_structผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
รหัส 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
- การนิยามชนิดกับการประกาศตัวแปรหนึ่งระเบียนต่างกันอย่างไร
- เหตุใด room[i].score จึงใช้ดัชนีก่อนจุด
- ทำไมคัดลอกทั้งระเบียนได้ แต่กำหนดค่าอาร์เรย์ชื่อทั้งก้อนด้วย = ภายหลังไม่ได้
- การแก้สมาชิกใน with_bonus จะเปลี่ยน original เมื่อใด
- ถ้าสลับเฉพาะคะแนน ข้อมูลใดอาจไม่ตรงกับความจริงของรายการ
- เมื่อค้นหาไม่พบ ต้องตรวจอะไรจึงจะไม่อ่านดัชนี -1
- การเพิ่มรายการควรเริ่มเปลี่ยนพื้นที่หลักหลังตรวจเงื่อนไขใดบ้าง
เมื่ออธิบายได้ ให้นำตารางรหัส ชื่อ และคะแนนไปใช้เป็นหน่วยข้อมูลที่จะเคลื่อนย้ายร่วมกันใน บทที่ 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 อย่างไร
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาโครงสร้างข้อมูลแบบระเบียนและการเข้าถึงสมาชิกเพิ่มเติมจาก Kernighan และ Ritchie (1988) บทที่ 6 ว่าด้วยโครงสร้าง (structures) แล้วทดลองเขียนโปรแกรมที่นิยามระเบียนของตนเองและแสดงสมาชิก
- ทบทวนข้อกำหนดเรื่องชนิดข้อมูลแบบโครงสร้างในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจการจัดวางสมาชิกของระเบียน
- ทดลองออกแบบโปรแกรมที่ใช้อาร์เรย์ของระเบียนเก็บข้อมูลหลายรายการ แล้วเพิ่มฟังก์ชันที่รับระเบียนไปแสดงผลหรือสรุปข้อมูล เพื่อฝึกการแยกหน้าที่ของฟังก์ชัน
เอกสารอ้างอิง
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 และข้อมูลสมมติในขอบเขตที่กำหนด ไม่ใช้ข้อมูลผู้เรียนจริง