Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 7 อาร์เรย์
แผนบริหารการสอนประจำบทที่ 7
บทนี้ว่าด้วยอาร์เรย์ (array) ของภาษา C ซึ่งเป็นโครงสร้างข้อมูลพื้นฐานที่เก็บข้อมูลชนิดเดียวกันหลายค่าไว้ในชื่อเดียว ต่อยอดจากการวนซ้ำด้วยลูปในบทที่ 5 และการออกแบบฟังก์ชันในบทที่ 6 โดยอธิบายการประกาศและการเข้าถึงสมาชิกอาร์เรย์หนึ่งมิติด้วยดัชนีที่เริ่มจาก 0 การประมวลผลอาร์เรย์ด้วยลูปอย่างระวังขอบเขตดัชนี การส่งอาร์เรย์เข้าฟังก์ชัน และอาร์เรย์สองมิติที่ประมวลผลด้วยลูปซ้อน เพื่อให้จัดเก็บและประมวลผลข้อมูลเป็นชุดได้อย่างถูกต้องและเป็นระบบ
| ผลลัพธ์ | ข้อความ | หลักฐาน | CLO |
|---|---|---|---|
| LLO7.1 | อธิบายและใช้อาร์เรย์หนึ่งมิติ (ประกาศ กำหนดค่าเริ่มต้น เข้าถึงสมาชิกด้วยดัชนี) | งานข้อ 1 (ปฏิบัติการที่ 7 Lab A) | CLO1 |
| LLO7.2 | ใช้ลูปประมวลผลอาร์เรย์ (ผลรวม ค่าเฉลี่ย ค้นหา) โดยระวังขอบเขตดัชนี | งานข้อ 2 (ปฏิบัติการที่ 7 Lab A) | CLO2 |
| LLO7.3 | ใช้อาร์เรย์สองมิติ และส่งอาร์เรย์เข้าฟังก์ชัน | งานข้อ 3 (ปฏิบัติการที่ 7 Lab B) | CLO2 |
| LLO7.4 | ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้ | โค้ดทุกข้อที่ส่งในบทนี้ | CLO5 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO1 อธิบายและใช้ตัวแปร ชนิดข้อมูล การรับ-แสดงผล และโครงสร้างควบคุม (if/else, switch, loop) เพื่อแก้ปัญหาและได้ผลลัพธ์ที่ถูกต้อง
- CLO2 อธิบายขั้นตอนการทำงาน (Logic) ของภาษาโปรแกรมและเลือกใช้โครงสร้างควบคุม (Control Structure) ให้สอดคล้องกับโจทย์ที่กำหนดได้
บทที่ 7 เป็นจุดที่นำการวนซ้ำจากบทที่ 5 และฟังก์ชันจากบทที่ 6 มาใช้กับข้อมูลชุดที่เก็บในอาร์เรย์ จึงเป็นสะพานเชื่อมไปสู่การจัดการสตริงในบทที่ 8 (สตริงคืออาร์เรย์ของอักขระ) โครงสร้างข้อมูลแบบระเบียนในบทที่ 9 และการเรียงลำดับและการค้นหาในบทที่ 10 งานทั้งสามข้อในบทนี้สะสมหลักฐานของ CLO1 และ CLO2 ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ นอกจากนี้บทนี้ยังทำหน้าที่เป็น จุดตรวจความซื่อสัตย์ทางวิชาการ จุดที่ 2 (CLO4 Checkpoint 2: ทบทวนความซื่อสัตย์ก่อนสอบกลางภาค) เพื่อให้ผู้เรียนสำรวจประวัติการเขียนโค้ดและแหล่งอ้างอิงในใบงานที่ 1–7 ก่อนเข้าสู่การสอบกลางภาค (เป็นจุดตรวจให้ข้อเสนอแนะ ไม่คิดสัดส่วนคะแนน) (ดูบันทึกสำหรับผู้สอนท้ายบท)
วิธีสอน
- ทบทวนการวนซ้ำด้วยลูป
forจากบทที่ 5 และการส่งค่าเข้าฟังก์ชันจากบทที่ 6 แล้วตั้งโจทย์ที่ต้องเก็บข้อมูลหลายค่า เช่น คะแนนนักศึกษาหลายคน เพื่อชี้ให้เห็นความจำเป็นของอาร์เรย์แทนการประกาศตัวแปรแยกหลายตัว - แนะนำการประกาศอาร์เรย์หนึ่งมิติและการเข้าถึงสมาชิกด้วยดัชนีที่เริ่มจาก 0 โดยสาธิตบนเครื่องจริง และย้ำว่าอาร์เรย์ขนาด n มีดัชนีตั้งแต่ 0 ถึง n-1
- สาธิตการวนประมวลผลอาร์เรย์ด้วยลูป
forเพื่อหาผลรวมและค่าเฉลี่ย พร้อมชี้กับดักการวนเกินขอบเขตดัชนี - อธิบายการส่งอาร์เรย์เข้าฟังก์ชันพร้อมขนาด และชี้ว่าฟังก์ชันทำงานกับอาร์เรย์เดิม ต่างจากการส่งตัวแปรเดี่ยวแบบสำเนาค่า (by value) ในบทที่ 6
- สาธิตอาร์เรย์สองมิติและลูปซ้อน แล้วให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
- เอกสารประกอบบทที่ 7 และสไลด์สรุปอาร์เรย์หนึ่งมิติ/สองมิติ และการส่งอาร์เรย์เข้าฟังก์ชัน
- ไฟล์โปรแกรมตัวอย่าง
ch7_array.cและch7_matrix.c
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 8 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 8 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | ประกาศอาร์เรย์คะแนน กำหนดค่าเริ่มต้น และแสดงสมาชิกทุกตัวด้วยลูป | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO7.1 | CLO1 |
| 2 | คำนวณผลรวมและค่าเฉลี่ยของอาร์เรย์ด้วยลูป โดยไม่เกินขอบเขตดัชนี | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO7.2 | CLO2 |
| 3 | ใช้อาร์เรย์สองมิติแสดงตาราง และเขียนฟังก์ชันที่รับอาร์เรย์หนึ่งมิติ | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO7.3 | CLO2 |
เงื่อนไขผ่าน: งานข้อ 2 ประเมินจากไฟล์เดียวกับงานข้อ 1 ที่ต่อยอดการวนประมวลผลอาร์เรย์ ดังนั้นงานข้อ 1 ต้องประกาศอาร์เรย์และแสดงสมาชิกครบถูกต้องและคอมไพล์ผ่านก่อนจึงจะประเมินข้อ 2 ได้
คะแนนใบงาน (10 คะแนน)
| ผลลัพธ์ | สิ่งที่ตรวจ | คะแนน |
|---|---|---|
| LLO7.1 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO7.2 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO7.3 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO7.4 | มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง | 1 |
ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา
เกณฑ์ให้คะแนน
งานข้อ 1 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:
| เกณฑ์ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| งานข้อ 1 | ประกาศและกำหนดค่าเริ่มต้นอาร์เรย์ถูก และแสดงสมาชิกครบทุกตัวด้วยลูป | เข้าถึงสมาชิกผิด หรือแสดงไม่ครบทุกตัว |
งานข้อ 2 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ (checklist): ผลรวมและค่าเฉลี่ยถูกต้อง, เงื่อนไขลูปอยู่ในช่วงดัชนี 0 ถึง n-1 ไม่เกินขอบเขต, ใช้ %.2f กับค่าเฉลี่ยที่เป็นทศนิยม, จัดย่อหน้า 4 ช่องว่างและมีคอมเมนต์ส่วนหัว
งานข้อ 3 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ: อาร์เรย์สองมิติแสดงตารางถูกต้อง, ส่งอาร์เรย์เข้าฟังก์ชันพร้อมขนาดถูกต้อง, คอมไพล์ผ่านไม่มีการแจ้งเตือน, เป็นไปตามมาตรฐานโค้ด
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
บทนำ
โปรแกรมจำนวนมากต้องจัดเก็บข้อมูลชนิดเดียวกันหลายค่า เช่น คะแนนของนักศึกษาทั้งห้อง หรือค่าที่วัดได้หลายครั้ง การประกาศตัวแปรแยกทีละตัวย่อมไม่สะดวกและขยายไม่ได้เมื่อจำนวนข้อมูลเพิ่มขึ้น อาร์เรย์ (array) จึงเข้ามาแก้ปัญหานี้ด้วยการเก็บข้อมูลชนิดเดียวกันหลายค่าไว้ในชื่อเดียว แล้วเข้าถึงแต่ละค่าด้วยดัชนี (index) บทที่ 5 ได้แนะนำการวนซ้ำด้วยลูป และบทที่ 6 ได้แนะนำฟังก์ชันไปแล้ว บทนี้นำทั้งสองเรื่องมาใช้กับข้อมูลชุดที่เก็บในอาร์เรย์ ทั้งอาร์เรย์หนึ่งมิติและสองมิติ รวมถึงการส่งอาร์เรย์เข้าฟังก์ชัน
ทักษะเรื่องอาร์เรย์ในบทนี้เป็นพื้นฐานของหลายบทที่ตามมา อาร์เรย์ของอักขระจะถูกใช้เป็นสตริงในบทที่ 8 แนวคิดการเก็บข้อมูลเป็นชุดจะต่อยอดเป็นโครงสร้างข้อมูลแบบระเบียนในบทที่ 9 และการวนประมวลผลอาร์เรย์เป็นหัวใจของการเรียงลำดับและการค้นหาในบทที่ 10
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
| อาร์เรย์หนึ่งมิติ ประกาศ กำหนดค่าเริ่มต้น และเข้าถึงด้วยดัชนีเริ่ม 0 | บท 8 (สตริง), บท 9 (struct), บท 10 (เรียง/ค้นหา) |
| การวนประมวลผลอาร์เรย์ด้วยลูปอย่างระวังขอบเขต | ทุกบทที่ประมวลผลข้อมูลเป็นชุด |
| การส่งอาร์เรย์เข้าฟังก์ชัน (arr[], ขนาด) และลูปซ้อนสำหรับอาร์เรย์สองมิติ | บท 10 (เรียง/ค้นหา) เป็นต้นไป |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE และต้องมีความรู้เรื่องการวนซ้ำด้วยลูป for จากบทที่ 5 การออกแบบฟังก์ชันและการส่งค่าเข้าฟังก์ชันจากบทที่ 6 และการแปลงชนิดข้อมูล (cast) จากบทที่ 2 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ผลการแสดงข้อความภาษาไทยอาจต่างกันเล็กน้อย แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน
วิธีใช้บทเรียนและใบงานในหน้านี้
ทบทวน บทที่ 6 ฟังก์ชันและการออกแบบแบบโมดูล โดยอธิบายข้อมูลเข้าและผลคืนของฟังก์ชัน จากนั้นเพิ่มคำถามว่า “ข้อมูลมีกี่สมาชิก และสมาชิกใดถูกตั้งค่าแล้ว”
ตารางใบงานเป็นแบบสำหรับคัดลงสมุดหรือเอกสารของตน ไม่ใช่ช่องส่งคำตอบบนเว็บไซต์ ให้ทำนาย → ไล่ดัชนี → คอมไพล์ → รัน → เปรียบเทียบ โดยไม่ลบสิ่งที่ทำนายผิดก่อนอธิบายสาเหตุ
ทุกชุดคะแนนและความพึงพอใจเป็นข้อมูลสมมติ ไม่ใช่ข้อมูลนักศึกษา เกณฑ์งานส่งและ LLO7.1–7.3 ยังคงเดิม กิจกรรมเพิ่มใช้ฝึกและให้ข้อมูลป้อนกลับ ส่วนเวลาและแนวคำตอบอยู่ในส่วนผู้สอน
ใช้ข้อมูลจำนวนเต็มที่แทนด้วย int ได้ ตรวจผลอ่านก่อนใช้ค่า และไม่รันโค้ดที่จงใจอ่านหรือเขียนนอกอาร์เรย์เพื่อคาดหวังอาการตายตัว การไม่แครชไม่ใช่หลักฐานว่าดัชนีถูกต้อง
7.1 อาร์เรย์หนึ่งมิติ: การประกาศและการเข้าถึง
ระดับที่ 1: อาร์เรย์ (array) คือโครงสร้างข้อมูลที่เก็บข้อมูลชนิดเดียวกันหลายค่าไว้ในชื่อเดียว แทนการประกาศตัวแปรแยกทีละตัว การเข้าถึงแต่ละค่าในอาร์เรย์ทำผ่านดัชนี (index) ซึ่งเป็นลำดับตำแหน่งของสมาชิก
ระดับที่ 2: การประกาศอาร์เรย์ระบุชนิดข้อมูล ชื่อ และจำนวนสมาชิกในวงเล็บเหลี่ยม เช่น int scores[5]; ประกาศอาร์เรย์ชื่อ scores ที่เก็บจำนวนเต็มได้ 5 ค่า และกำหนดค่าเริ่มต้นพร้อมประกาศได้ด้วยวงเล็บปีกกา เช่น int scores[5] = {80, 75, 90, 65, 100}; การเข้าถึงสมาชิกใช้ชื่ออาร์เรย์ตามด้วยดัชนีในวงเล็บเหลี่ยม โดยดัชนีเริ่มจาก 0 ดังนั้น scores[0] คือสมาชิกตัวแรก และ scores[4] คือสมาชิกตัวที่ห้า
int scores[5] = {80, 75, 90, 65, 100};
printf("%d\n", scores[0]);ระดับที่ 3: อาร์เรย์ขนาด n มีดัชนีที่ใช้ได้ตั้งแต่ 0 ถึง n-1 เท่านั้น ดังนั้นอาร์เรย์ขนาด 5 มีดัชนี 0 ถึง 4 การเข้าถึงดัชนีที่เกินช่วงนี้ เช่น scores[5] ในอาร์เรย์ขนาด 5 เป็นข้อผิดพลาดที่พบบ่อย เพราะภาษา C ไม่ตรวจขอบเขตดัชนีให้โดยอัตโนมัติ โปรแกรมจึงอาจอ่านหรือเขียนทับหน่วยความจำนอกอาร์เรย์โดยไม่แจ้งเตือน ซึ่งทำให้ได้ผลลัพธ์ที่คาดเดาไม่ได้ การระวังช่วงดัชนีให้อยู่ระหว่าง 0 ถึง n-1 จึงสำคัญทุกครั้งที่เข้าถึงสมาชิก
คำถามตรวจความเข้าใจ 7.1: อาร์เรย์ขนาด 5 มีดัชนีที่ใช้ได้ตั้งแต่เท่าใดถึงเท่าใด และสมาชิกตัวแรกเข้าถึงด้วยดัชนีใด
ใบงาน 1: แยกความจุ จำนวนข้อมูล และดัชนี
วาดช่องเก็บข้อมูลสิบช่องสำหรับ int scores[10]; แล้วใส่เพียง 40, 60, 50, 80 และ 70 กำกับดัชนีไว้เหนือช่อง อย่าเติมศูนย์ในช่องที่ยังไม่ได้ตั้งค่าแล้วนำมาคิดเป็นข้อมูลจริง
| สถานะ | ความจุ | count | ดัชนีตัวแรกที่มีข้อมูล | ดัชนีตัวสุดท้ายที่มีข้อมูล | ตำแหน่งที่จะเก็บถัดไปถ้ายังมีที่ |
|---|---|---|---|---|---|
| ยังไม่รับข้อมูล | 10 | 0 | |||
| รับแล้วหนึ่งค่า | 10 | 1 | |||
| รับแล้วห้าค่า | 10 | 5 | |||
| รับครบสิบค่า | 10 | 10 |
เมื่อ count เป็น 0 จะไม่มีสมาชิกข้อมูลจริงให้เรียกว่าตัวสุดท้าย สูตร count - 1 จึงไม่ใช่ดัชนีที่นำไปอ่านได้ในกรณีว่าง เมื่อ count เท่าความจุ ตำแหน่งถัดไปก็ไม่ใช่สมาชิกที่เขียนได้
อภิปรายว่า int values[5] = {0}; ตั้งค่าเริ่มต้นครบเป็นศูนย์ต่างจาก int values[5]; ภายในบล็อกอย่างไร แม้ตั้งศูนย์ครบก็ยังต้องแยกว่า “มีพื้นที่พร้อมใช้” กับ “ผู้ใช้ส่งข้อมูลจริงกี่ค่า”
7.2 การประมวลผลอาร์เรย์ด้วยลูป
ระดับที่ 1: การประมวลผลอาร์เรย์มักทำโดยวนทีละสมาชิกตั้งแต่ตัวแรกถึงตัวสุดท้ายด้วยลูป for จากบทที่ 5 โดยใช้ตัวแปรนับเป็นดัชนีของอาร์เรย์
ระดับที่ 2: รูปแบบที่ใช้บ่อยคือให้ตัวนับ i เริ่มที่ 0 และวนขณะที่ i ยังน้อยกว่าขนาดอาร์เรย์ ดังเงื่อนไข i < size แล้วเข้าถึงสมาชิกด้วย arr[i] การหาผลรวมทำด้วยตัวแปรสะสมจากบทที่ 5 ด้วย sum += arr[i]; และค่าเฉลี่ยคำนวณจากผลรวมหารด้วยจำนวนสมาชิก โดยแปลงชนิดเป็นทศนิยมด้วยการ cast จากบทที่ 2 เป็น (double)total / size เพื่อไม่ให้ปัดเศษเป็นจำนวนเต็ม
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += scores[i];
}การค้นหาสมาชิกทำได้โดยวนเทียบทีละตัวกับค่าที่ต้องการ และหยุดด้วย break เมื่อพบ ตามที่ฝึกในบทที่ 5
ระดับที่ 3: กับดักที่พบบ่อยคือความคลาดเคลื่อนที่ขอบเขต (off-by-one) เมื่อตั้งเงื่อนไขลูปไม่พอดีกับขนาดอาร์เรย์ เช่น การใช้ i <= size แทน i < size ทำให้วนไปถึงดัชนี size ซึ่งเกินสมาชิกตัวสุดท้าย (ดัชนีสูงสุดคือ n-1) และเข้าถึงนอกขอบเขต การตั้งเงื่อนไขให้ตัวนับน้อยกว่าขนาดอาร์เรย์เสมอ และตรวจค่าดัชนีในรอบแรกและรอบสุดท้าย ช่วยลดข้อผิดพลาดนี้ได้
คำถามตรวจความเข้าใจ 7.2: การเข้าถึง scores[5] ในอาร์เรย์ขนาด 5 เกิดปัญหาอะไร และควรตั้งเงื่อนไขลูปอย่างไรจึงจะไม่เกินขอบเขต
ใบงาน 2: นับการเข้าถึงให้ถึงสมาชิกสุดท้ายพอดี
ใช้ข้อมูล Lab A แล้วเติมค่าที่ถูกอ่านและ sum หลังแต่ละรอบ โดยเริ่ม sum ที่ 0 การตรวจครั้งที่ i เป็น 5 มีไว้จบลูป ไม่ใช่ให้อ่าน scores[5]
| i ที่ตรวจ | i < 5 | สมาชิกที่อ่านได้ในรอบนี้ | sum หลังบวก |
|---|---|---|---|
| 0 | |||
| 1 | |||
| 2 | |||
| 3 | |||
| 4 | |||
| 5 |
เขียนรายการดัชนีของลูปที่เริ่ม 1 และใช้ i < 5 เปรียบเทียบกับฉบับเริ่ม 0 ทีละอย่าง หากแก้เป็น i <= 5 ให้ระบุตำแหน่งผิดบนกระดาษ ไม่ต้องรันการเข้าถึงนอกอาร์เรย์เพื่อสาธิต
ส่วนตัวอย่าง mean_score ใช้ count เป็นบวกและไม่เกินจำนวนสมาชิกจริง พร้อมผลรวมที่อยู่ใน int สำหรับชุดฝึก 0–100 ไม่เกินสิบค่า ผลรวมสูงสุด 1000 จึงไม่ล้นชนิดและแปลงเป็น double ก่อนหารได้
ตัวอย่างค้นหา: ตำแหน่งศูนย์ไม่ใช่ไม่พบ
สร้างไฟล์ find_first.c ให้ฟังก์ชันคืนดัชนีแรกที่พบ และคืน -1 เมื่อไม่พบ สัญญากำหนด count ไม่ติดลบและไม่เกินจำนวนสมาชิกที่ใช้ได้ ฟังก์ชันไม่แก้ข้อมูลผ่านพารามิเตอร์ const
/* find_first.c : first matching index, or -1 */
#include <stdio.h>
int find_first(const int values[], int count, int target);
int main(void)
{
const int values[5] = {40, 60, 40, 80, 70};
int target;
if (scanf("%d", &target) != 1) {
puts("Input error");
return 1;
}
int index = find_first(values, 5, target);
if (index == -1) {
puts("Not found");
} else {
printf("index=%d value=%d\n", index, values[index]);
}
return 0;
}
/* Read only values[0] through values[count - 1]. */
int find_first(const int values[], int count, int target)
{
for (int i = 0; i < count; ++i) {
if (values[i] == target) {
return i;
}
}
return -1;
}ก่อนรันให้ทำนายเมื่อค้นหา 40, 70 และ 99 ระบุว่าข้อมูล 40 ที่มีสองตำแหน่งจะเลือกตัวใด การตรวจเพียง if (index) ไม่ตรงสัญญา เพราะดัชนี 0 เป็นคำตอบที่ถูกต้องและ -1 ไม่ใช่ศูนย์
ทดลองส่ง count เป็น 0 ในสำเนาที่เรียกฟังก์ชันด้วยข้อมูลจริงที่มีพื้นที่อยู่ ฟังก์ชันต้องไม่อ่านสมาชิกและคืนไม่พบ อย่าส่ง count ใหญ่กว่าพื้นที่โดยหวังให้ฟังก์ชันรู้ความจุเอง
7.3 การส่งอาร์เรย์เข้าฟังก์ชัน
ระดับที่ 1: ฟังก์ชันจากบทที่ 6 สามารถรับอาร์เรย์เป็นพารามิเตอร์ได้ เพื่อแยกการประมวลผลอาร์เรย์ออกเป็นโมดูลที่นำกลับมาใช้ซ้ำได้ แต่ต้องส่งขนาดของอาร์เรย์ไปพร้อมกันเสมอ
ระดับที่ 2: การประกาศพารามิเตอร์อาร์เรย์ใช้วงเล็บเหลี่ยมว่างตามชื่อ เช่น int sum_array(int arr[], int size) รับอาร์เรย์จำนวนเต็ม arr และขนาด size เหตุที่ต้องส่งขนาดไปด้วยเพราะฟังก์ชันไม่สามารถทราบจำนวนสมาชิกของอาร์เรย์จากตัวพารามิเตอร์เองได้ การวนประมวลผลภายในฟังก์ชันจึงต้องอาศัย size เป็นขอบเขต
int sum_array(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}ระดับที่ 3: จุดสำคัญที่ต่างจากบทที่ 6 คือ เมื่อส่งอาร์เรย์เข้าฟังก์ชัน ฟังก์ชันจะทำงานกับอาร์เรย์เดิมโดยตรง ไม่ใช่สำเนาของอาร์เรย์ ดังนั้นถ้าฟังก์ชันแก้ค่าสมาชิก ค่าในอาร์เรย์เดิมจะเปลี่ยนตามไปด้วย แต่ภาษา C ยังคงส่งค่าแบบ by value เช่นเดิม พารามิเตอร์อาร์เรย์ถูกปรับเป็นพอยน์เตอร์และรับสำเนาค่าที่ชี้ไปยังสมาชิกชุดเดิม จึงแก้สมาชิกผ่านค่านั้นได้ สาเหตุเชิงลึกของพฤติกรรมนี้เกี่ยวข้องกับทฤษฎีตัวชี้ (pointer) ซึ่งเป็นหัวข้อเสริมที่จะศึกษาต่อภายหลัง ในบทนี้เพียงจำหลักว่าฟังก์ชันที่รับอาร์เรย์ทำงานกับอาร์เรย์เดิมได้ก็เพียงพอ
คำถามตรวจความเข้าใจ 7.3: เหตุใดการส่งอาร์เรย์เข้าฟังก์ชันจึงต้องส่งขนาดไปด้วย และผลของการแก้ค่าสมาชิกภายในฟังก์ชันต่างจากการส่งตัวแปรเดี่ยวแบบส่งสำเนาค่าอย่างไร
7.4 อาร์เรย์สองมิติ
ระดับที่ 1: อาร์เรย์สองมิติใช้เก็บข้อมูลที่จัดเป็นตารางแบบแถวและหลัก เช่น ตารางตัวเลขหรือเมทริกซ์ โดยมองเป็นอาร์เรย์ของแถว ที่แต่ละแถวมีหลายหลัก
ระดับที่ 2: การประกาศอาร์เรย์สองมิติระบุจำนวนแถวและจำนวนหลักในวงเล็บเหลี่ยมสองชุด เช่น int table[2][3] ประกาศตาราง 2 แถว 3 หลัก และกำหนดค่าเริ่มต้นด้วยวงเล็บปีกกาซ้อน เช่น int table[2][3] = { {1, 2, 3}, {4, 5, 6} }; การเข้าถึงสมาชิกใช้สองดัชนี table[r][c] โดย r คือดัชนีแถวและ c คือดัชนีหลัก การวนทุกสมาชิกจึงใช้ลูปซ้อน (nested loop) คือลูปนอกวนแถวและลูปในวนหลัก
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 3; c++) {
printf("%d ", table[r][c]);
}
printf("\n");
}ระดับที่ 3: ภาษา C จัดเก็บอาร์เรย์สองมิติแบบเรียงตามแถว (row-major) คือสมาชิกของแถวเดียวกันอยู่ติดกันในหน่วยความจำ ลูปนอกจึงควรวนตามแถวและลูปในวนตามหลักเพื่อเข้าถึงตามลำดับที่เป็นธรรมชาติ ดัชนีทั้งสองมิติต่างมีช่วงตั้งแต่ 0 ถึงจำนวนในมิตินั้นลบหนึ่ง การสลับแถวกับหลักหรือการวนลูปซ้อนเกินขอบเขตเป็นข้อผิดพลาดที่พบบ่อย อาร์เรย์สองมิติใช้แทนตารางคะแนน ตารางเวลา หรือเมทริกซ์ในงานคำนวณจริง
คำถามตรวจความเข้าใจ 7.4: อาร์เรย์ int table[2][3] มีกี่แถวและกี่หลัก และการวนแสดงสมาชิกทุกตัวต้องใช้ลูปลักษณะใด
ตัวอย่างพร้อมใช้: ปฏิเสธรายการที่เกินความจุก่อนเขียน
สร้างไฟล์ bounded_store.c รับจำนวนคำขอ 0–11 ครั้ง แล้วรับค่าครั้งละหนึ่งจำนวนช่วง -100 ถึง 100 เก็บได้ไม่เกินสิบค่า รายการที่สิบเอ็ดถูกอ่านและตรวจช่วงก่อนปฏิเสธ แต่ไม่เปลี่ยน count หรือข้อมูลเดิม
/* bounded_store.c : check capacity before storing */
#include <stdio.h>
int main(void)
{
int values[10];
int count = 0;
int attempts;
if (scanf("%d", &attempts) != 1) {
puts("Input error");
return 1;
}
if (attempts < 0 || attempts > 11) {
puts("Range error");
return 1;
}
for (int attempt = 0; attempt < attempts; ++attempt) {
int value;
if (scanf("%d", &value) != 1) {
puts("Input error");
return 1;
}
if (value < -100 || value > 100) {
puts("Range error");
return 1;
}
if (count == 10) {
puts("Full");
continue;
}
values[count] = value;
++count;
}
printf("count=%d values:", count);
int sum = 0;
for (int i = 0; i < count; ++i) {
printf(" %d", values[i]);
sum += values[i];
}
puts("");
if (count == 0) {
puts("No data");
} else {
printf("sum=%d mean=%.2f\n", sum, (double)sum / count);
}
return 0;
}| กรณีทดสอบ | สิ่งที่ต้องตรวจ | ทำนายก่อนรัน | ผลจริง |
|---|---|---|---|
| attempts เป็น 0 | ไม่อ่านสมาชิกและไม่หารศูนย์ | ||
| หนึ่งค่าเป็น -5 | ใช้ดัชนี 0 และเฉลี่ยได้ค่าลบ | ||
| ครบสิบค่า | count เท่ากับความจุ | ||
| ขอสิบเอ็ดค่า โดยค่าท้ายเป็น 99 | มี Full และสิบค่าเดิมยังอยู่ | ||
| ข้อมูลหมดก่อนครบคำขอ | จบด้วย Input error |
แยก attempt ซึ่งนับจำนวนครั้งที่ลองจาก count ซึ่งนับจำนวนค่าที่เก็บจริง ลูปสรุปใช้ count ไม่ใช่ attempts หรือความจุสิบเสมอ การตรวจ count == 10 เพียงพอในตัวอย่างนี้เพราะเริ่ม 0 และเพิ่มหลังเก็บสำเร็จทีละหนึ่งเท่านั้น
7.5 กรณีศึกษาต่อเนื่อง: เมื่อจำเป็นต้องเก็บคะแนนไว้ใช้หลายครั้ง
ในบทลูปเราหาผลรวมได้ขณะรับข้อมูลทีละค่าโดยไม่ต้องเก็บทั้งหมด แต่ถ้าต้องรายงานว่าคะแนนแต่ละรายการต่างจากค่าเฉลี่ยเท่าไร เราต้องรู้ค่าเฉลี่ยก่อนแล้วจึงกลับมาใช้คะแนนเดิม อาร์เรย์ช่วยเก็บข้อมูลหลายค่าที่มีชนิดเดียวกันเพื่อใช้งานซ้ำ ไม่ใช่ทุกโจทย์ที่มีหลายค่าจำเป็นต้องใช้อาร์เรย์ ต้องพิจารณาว่าต้องย้อนกลับมาใช้ข้อมูลหรือไม่
7.5.1 ความจุ จำนวนที่ใช้ และดัชนีเป็นสามแนวคิด
กำหนด int scores[10]; หมายถึงมีพื้นที่สิบสมาชิก แต่หากรับคะแนนจริงเพียงห้าค่า จำนวนที่ใช้คือห้า ดัชนีที่ใช้เข้าถึงข้อมูลจริงจึงเป็น 0–4 ไม่ใช่ 0–9 ทั้งหมด สมาชิกที่ยังไม่ได้ตั้งค่าไม่ควรถูกนำมาคำนวณเพียงเพราะอยู่ในพื้นที่อาร์เรย์
GNU C: Accessing Array Elements ระบุว่าดัชนีเริ่มศูนย์และ C ไม่ตรวจขอบเขตให้อัตโนมัติ การอ่านหรือเขียนนอกขอบเขตไม่ได้รับประกันว่าจะหยุดพร้อมข้อความเตือน อาจดูเหมือนทำงานได้ในบางครั้ง จึงไม่ควรใช้การ “ไม่แครช” เป็นหลักฐานว่าดัชนีถูก
| สิ่งที่กำลังนับ | ตัวอย่างค่า | ใช้เพื่ออะไร |
|---|---|---|
| ความจุที่จัดไว้ | 10 | จำกัดจำนวนข้อมูลสูงสุด |
| จำนวนข้อมูลจริง | 5 | กำหนดช่วงที่มีความหมาย |
| ดัชนีของสมาชิกสุดท้ายที่ใช้ | 4 | เข้าถึงค่าตัวสุดท้าย |
| จำนวนครั้งที่วนครบข้อมูล | 5 | ประมวลผลสมาชิก 0–4 |
7.5.2 ประมวลผลสองรอบด้วยเหตุผลที่ต่างกัน
#include <stdio.h>
double mean_score(const int scores[], int count)
{
int sum = 0;
for (int i = 0; i < count; ++i) {
sum += scores[i];
}
return (double)sum / count;
}
int main(void)
{
const int scores[] = {40, 60, 50, 80, 70};
const int count = 5;
double mean = mean_score(scores, count);
printf("mean=%.2f\n", mean);
for (int i = 0; i < count; ++i) {
printf("%d: score=%d difference=%+.2f\n",
i, scores[i], scores[i] - mean);
}
return 0;
}ผลเฉลี่ยเป็น 60.00 ส่วนต่างตามลำดับเป็น -20.00, +0.00, -10.00, +20.00 และ +10.00 รอบแรกสะสมเพื่อให้ได้ข้อมูลภาพรวม รอบที่สองใช้ภาพรวมนั้นกับสมาชิกแต่ละตัว ฟังก์ชันกำหนดข้อตกลงว่า count ต้องเป็นบวกและไม่เกินจำนวนสมาชิกที่ใช้ได้ ผู้เรียกในตัวอย่างส่งห้าแน่นอน ถ้าออกแบบเป็นฟังก์ชันทั่วไปต้องเพิ่มวิธีรายงานเมื่อไม่มีข้อมูล
ไม่ควรคำนวณค่าเฉลี่ยใหม่ภายในทุกรอบของการแสดงส่วนต่าง เพราะข้อมูลชุดเดิมไม่เปลี่ยน การคำนวณซ้ำจะทำงานเพิ่มโดยไม่ได้คำตอบใหม่ ให้ลองนับจำนวนครั้งที่อ่านสมาชิกในสองวิธี เพื่อเตรียมความเข้าใจเรื่องต้นทุนอัลกอริทึมในบทที่ 12
ใบงานออกแบบ: เลือกเก็บทุกค่าหรือเก็บเฉพาะสรุป
เทียบงานสามแบบ ได้แก่รายงานผลรวมอย่างเดียว รายงานส่วนต่างจากค่าเฉลี่ยของแต่ละค่า และนับความถี่ระดับ 1–5 การมีข้อมูลหลายค่าไม่ได้บังคับให้เก็บรายการต้นฉบับทั้งหมดเสมอ
| งาน | ต้องย้อนใช้ค่าเดิมหรือไม่ | ข้อมูลที่ต้องเก็บ | สิ่งที่กู้กลับไม่ได้จากสรุป |
|---|---|---|---|
| ผลรวมอย่างเดียว | |||
| ส่วนต่างของแต่ละค่าจากค่าเฉลี่ย | |||
| ความถี่ระดับ 1–5 |
ใช้ข้อมูล 40 60 50 80 70 ไล่สองรอบของโปรแกรม mean_score รอบแรกหาค่าเฉลี่ย รอบที่สองอ่านแต่ละค่าเพื่อหาส่วนต่าง เปรียบเทียบกับแบบเรียก mean_score ใหม่ทุกครั้ง แม้ได้ตัวเลขเดียวกันก็อ่านสมาชิกซ้ำมากกว่า
ก่อนเพิ่มพื้นที่เก็บ ให้ระบุความจุ จำนวนที่ใช้จริง และสิ่งที่ต้องทำเมื่อเต็มไว้ในสัญญา อย่าแก้ปัญหาข้อมูลเกินด้วยการขยายอาร์เรย์โดยยังไม่มีขอบเขตของงาน
7.6 การแก้สมาชิกผ่านฟังก์ชันและตารางสองมิติ
7.6.1 ส่งความยาวไปด้วย ไม่เดาขนาดจากพารามิเตอร์
เมื่อประกาศพารามิเตอร์เป็น int values[] ฟังก์ชันไม่ได้รับสำเนาอาร์เรย์ทั้งก้อนแบบตัวแปร int ในบทก่อน แต่เข้าถึงสมาชิกชุดเดิมได้ การแก้ values[0] จึงอาจเปลี่ยนข้อมูลที่ผู้เรียกเห็น ความเข้าใจนี้ไม่ขัดกับการส่งค่าใน C เพราะพารามิเตอร์อาร์เรย์ถูกปรับเป็นชนิดพอยน์เตอร์ รายละเอียดพอยน์เตอร์จะเรียนต่อภายหลัง ในระดับนี้ใช้กฎว่าต้องส่งจำนวนสมาชิกและระบุให้ชัดว่าฟังก์ชันอ่านหรือแก้ข้อมูล
การใช้ sizeof values / sizeof values[0] ภายในฟังก์ชันลักษณะนี้ไม่ใช่วิธีหาจำนวนสมาชิกเดิม ส่วน sizeof scores / sizeof scores[0] ใช้ได้ในขอบเขตที่ scores ยังเป็นอาร์เรย์จริง ควรอธิบายตามชนิดของนิพจน์ ไม่ใช้กฎจำว่า sizeof หาความยาวอาร์เรย์ได้ทุกแห่ง
ฟังก์ชันหาผลรวมสามารถรับ const int values[] เพื่อแสดงเจตนาว่าไม่แก้สมาชิกผ่านพารามิเตอร์นี้ แต่ const ไม่ทำหน้าที่ตรวจดัชนี ผู้เขียนยังต้องรักษาขอบเขตและส่งจำนวนที่ถูกต้อง
7.6.2 อาร์เรย์สองมิติต้องนิยามความหมายของแถวและคอลัมน์
สมมติคะแนนสามกลุ่มจากกิจกรรมสองครั้ง เป็นตารางแถวละกลุ่ม คอลัมน์ละกิจกรรม ข้อมูล {{8, 7}, {6, 9}, {10, 5}} ให้ผลรวมรายกลุ่ม 15, 15 และ 15 แต่ผลรวมรายกิจกรรมเป็น 24 และ 21 การใช้ข้อมูลชุดนี้ช่วยให้เห็นว่าผลรวมสองทิศอาจต่างกัน แม้รายกลุ่มดูเหมือนกันทั้งหมด
ก่อนเขียนลูปซ้อน ให้กำกับตารางว่า scores[group][activity] แล้ว trace วงรอบนอกกับวงรอบใน หากต้องการผลรวมรายกลุ่ม ให้รีเซ็ตตัวสะสมเมื่อเริ่มกลุ่มใหม่ หากต้องการผลรวมทั้งตาราง ให้ตั้งตัวสะสมก่อนทั้งสองลูป ตำแหน่งเริ่มค่าจึงสะท้อนขอบเขตของสิ่งที่ต้องการรวม
ใบงาน 3: แก้สมาชิกกับแก้ตัวนับเป็นคนละเรื่อง
สร้างไฟล์ array_update.c รับจำนวนสมาชิกที่จะใช้ช่วง 0–3 อาร์เรย์ตั้งค่าไว้ครบสามค่าเพื่อให้สังเกตได้ว่าการเปลี่ยน count ไม่ใช่การลบสมาชิกออกจากพื้นที่
/* array_update.c : shared elements and a copied count */
#include <stdio.h>
void update_first(int values[], int count);
int main(void)
{
int values[3] = {10, 20, 30};
int count;
if (scanf("%d", &count) != 1) {
puts("Input error");
return 1;
}
if (count < 0 || count > 3) {
puts("Range error");
return 1;
}
update_first(values, count);
printf("caller count=%d first=%d\n", count, values[0]);
return 0;
}
/* Write one element only when the caller has at least one item. */
void update_first(int values[], int count)
{
if (count == 0) {
puts("No update");
return;
}
values[0] = 99;
count = 0;
printf("inside count=%d\n", count);
}| count ที่ส่ง | แก้ values[0] หรือไม่ | count ภายในหลังแก้ | count ที่ผู้เรียกเห็น | values[0] ที่ผู้เรียกเห็น |
|---|---|---|---|---|
| 0 | ||||
| 1 | ||||
| 3 |
การส่งอาร์เรย์ไม่ได้สร้างสำเนาสมาชิกทั้งก้อนให้ฟังก์ชัน พารามิเตอร์ถูกปรับเป็นพอยน์เตอร์ที่เข้าถึงสมาชิกเดิม ส่วน count ยังเป็นสำเนาจำนวนเต็ม การแก้ count ในฟังก์ชันจึงไม่เปลี่ยน count ของ main
ในกรณี count เป็น 0 main ยังแสดง values[0] ได้ในโปรแกรมนี้เพราะมีพื้นที่และตั้งค่าเป็น 10 แล้ว แต่ค่านั้นไม่ถือเป็นข้อมูลที่รับไว้ใช้งานตาม count อย่านำตัวอย่างนี้ไปอ่านสมาชิกแรกของอาร์เรย์ที่ยังไม่ได้ตั้งค่า
ใบงาน 4: รวมแถวและคอลัมน์จากตารางเดียวกัน
สร้างไฟล์ matrix_totals.c รับข้อมูลหกค่าช่วง 0–100 ใส่ตาราง 3 แถว 2 คอลัมน์ กำหนดให้แถวแทนกลุ่มและคอลัมน์แทนกิจกรรม ต้องระบุความหมายก่อนเลือกทิศที่รวม
/* matrix_totals.c : row totals and column totals */
#include <stdio.h>
int row_total(int table[][2], int row);
int column_total(int table[][2], int rows, int column);
int main(void)
{
int table[3][2];
for (int r = 0; r < 3; ++r) {
for (int c = 0; c < 2; ++c) {
if (scanf("%d", &table[r][c]) != 1) {
puts("Input error");
return 1;
}
if (table[r][c] < 0 || table[r][c] > 100) {
puts("Range error");
return 1;
}
}
}
for (int r = 0; r < 3; ++r) {
printf("row[%d]=%d\n", r, row_total(table, r));
}
for (int c = 0; c < 2; ++c) {
printf("column[%d]=%d\n", c, column_total(table, 3, c));
}
return 0;
}
/* Read one valid row; the caller supplies row 0 through 2. */
int row_total(int table[][2], int row)
{
int sum = 0;
for (int c = 0; c < 2; ++c) {
sum += table[row][c];
}
return sum;
}
/* Read a valid column; rows must not exceed the actual table. */
int column_total(int table[][2], int rows, int column)
{
int sum = 0;
for (int r = 0; r < rows; ++r) {
sum += table[r][column];
}
return sum;
}ใช้ข้อมูล 8 7 6 9 10 5 และ 1 2 3 4 5 6 ก่อนรันให้วาดตารางจริง แล้วทำนายผลรวมแถวและคอลัมน์ด้วยมือ ผลรวมทุกแถวควรเท่าผลรวมทุกคอลัมน์ แต่ผลรวมของแต่ละแถวไม่จำเป็นต้องเท่ากัน
พารามิเตอร์ table[][2] บอกความกว้างแต่ละแถวเป็นสองสมาชิก ไม่ใช่ประกาศพื้นที่ตารางใหม่ในฟังก์ชัน ทั้งสองฟังก์ชันนี้อ่านตารางโดยไม่แก้ และผู้เรียกต้องส่งแถว/คอลัมน์ที่ถูกต้อง ไม่มีการตรวจความจุให้อัตโนมัติ
การย้ายคำสั่งเริ่ม sum ออกไปก่อนการรวมทุกแถว จะเปลี่ยนความหมายเป็นยอดสะสมข้ามแถว ให้ชี้ตำแหน่งเริ่มค่าที่สอดคล้องกับหนึ่งแถวในโค้ดก่อนแก้
7.7 เวิร์กช็อปขยาย: ตรวจความจุและเปรียบเทียบวิธีเก็บข้อมูล
ออกแบบโปรแกรมเก็บคะแนนสมมติไม่เกินสิบรายการ ใช้ตัวแปร count แยกจากความจุ หลังเพิ่มหนึ่งรายการให้เพิ่ม count เมื่อเก็บสำเร็จเท่านั้น ก่อนเขียนสมาชิกใหม่ตรวจว่าพื้นที่ยังเหลือ ห้ามทดสอบการเกินขอบเขตด้วยการจงใจเขียนนอกอาร์เรย์แล้วสังเกตว่าจะเกิดอะไรขึ้น
ทดสอบชุดว่าง หนึ่งรายการ เต็มสิบรายการ และคำขอเพิ่มรายการที่สิบเอ็ด สำหรับชุดว่างต้องมีผลรายงานที่ชัดโดยไม่หารด้วยศูนย์ สำหรับรายการที่สิบเอ็ดต้องปฏิเสธโดยไม่เปลี่ยนสิบค่าที่เก็บไว้เดิม บันทึก count ก่อนและหลังการเพิ่มทุกครั้ง
จากนั้นเพิ่มการค้นหาค่าสูงสุดของคะแนนที่อาจเป็นศูนย์ทั้งหมดและข้อมูลอุณหภูมิสมมติที่ติดลบทั้งหมด เปรียบเทียบวิธีเริ่มค่าสูงสุดที่ศูนย์กับเริ่มจากสมาชิกแรก จะเห็นว่าค่าเริ่มต้นขึ้นอยู่กับขอบเขตข้อมูล ไม่ใช่สูตรตายตัว ทั้งสองกรณีต้องตรวจว่ามีสมาชิกก่อนอ่านตัวแรก
งานสุดท้ายทำรายงานสองแบบจากตารางสองมิติเดียวกัน ได้แก่ผลรวมรายแถวและรายคอลัมน์ ส่งตาราง trace อย่างน้อยหนึ่งรอบเต็มของวงรอบใน พร้อมอธิบายว่าตัวสะสมถูกตั้งใหม่ตรงไหน การอธิบายความหมายของดัชนีและช่วงที่ใช้จริงสำคัญกว่าการได้ผลรวมเพียงอย่างเดียว
7.8 ตัวอย่างเปรียบเทียบ: นับความถี่โดยให้ดัชนีมีความหมาย
สมมติกิจกรรมมีระดับความพึงพอใจ 1–5 และข้อมูลสมมติคือ 5, 4, 5, 3, 4, 5 ต้องรายงานว่ามีแต่ละระดับกี่ครั้ง อาร์เรย์ความถี่ห้าช่องสามารถกำหนดให้ดัชนี 0 แทนระดับ 1 ไปจนดัชนี 4 แทนระดับ 5 ต้องเขียนข้อตกลงนี้ก่อนใช้สูตร frequency[rating - 1]
เริ่มทุกความถี่ที่ศูนย์ หลังรับค่า 5 เพิ่มช่องดัชนี 4 หลังค่า 4 เพิ่มช่อง 3 และทำต่อจนครบ จะได้ความถี่ 0, 0, 1, 2, 3 ตามระดับ 1–5 ผลรวมของความถี่ต้องเท่าจำนวนข้อมูลที่ยอมรับคือหก ความสัมพันธ์นี้เป็นจุดตรวจที่ช่วยพบการลืมเพิ่มหรือเพิ่มซ้ำ
| ระดับ | ดัชนี | ความถี่สุดท้าย |
|---|---|---|
| 1 | 0 | 0 |
| 2 | 1 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 2 |
| 5 | 4 | 3 |
ต้องตรวจ rating อยู่ใน 1–5 ก่อนลบหนึ่งแล้วใช้เป็นดัชนี หากรับศูนย์ ดัชนีจะเป็น -1 และถ้ารับหกจะเป็น 5 ซึ่งพ้นห้าช่อง ไม่ควรแก้ด้วยการเพิ่มขนาดอาร์เรย์เผื่อโดยไม่มีความหมาย เพราะข้อมูลยังผิดข้อกำหนด แม้บังเอิญไม่พ้นพื้นที่
เปรียบเทียบสองการออกแบบ แบบแรกใช้ห้าช่องและแปลงดัชนี แบบที่สองใช้หกช่องโดยไม่ใช้ช่องศูนย์ เพื่อให้ระดับตรงกับดัชนี ทั้งสองทำได้ถ้าระบุขอบเขตและลูปถูก เลือกจากความอ่านง่ายและต้นทุนที่ยอมรับ ไม่สรุปว่าการเริ่มดัชนีศูนย์บังคับให้ความหมายของข้อมูลต้องเริ่มศูนย์ด้วย
ท้ายงานเพิ่มข้อมูลผิดช่วงหนึ่งค่าและกำหนดว่าจะปฏิเสธหรือไม่นับ บันทึกจำนวนที่ป้อน จำนวนที่รับ และผลรวมความถี่ให้สอดคล้องกัน แล้วอธิบายว่าความถี่เป็นการสรุปข้อมูล จึงไม่สามารถย้อนกลับไปรู้ลำดับคะแนนเดิมได้ทุกกรณี ต่างจากการเก็บข้อมูลรายตัว
ใบงาน 5: แปลงระดับเป็นดัชนีหลังตรวจช่วง
สร้างไฟล์ rating_frequency.c อ่านข้อมูลหกครั้ง รับระดับ 1–5 และข้ามจำนวนเต็มผิดช่วง รายงานทั้งจำนวนที่ยอมรับกับความถี่ จึงแยก “อ่านหกครั้ง” ออกจาก “ได้ข้อมูลถูกหกค่า”
/* rating_frequency.c : validate before converting to an index */
#include <stdio.h>
int main(void)
{
int frequency[5] = {0};
int accepted = 0;
for (int attempt = 0; attempt < 6; ++attempt) {
int rating;
if (scanf("%d", &rating) != 1) {
puts("Input error");
return 1;
}
if (rating < 1 || rating > 5) {
puts("Skipped");
continue;
}
++frequency[rating - 1];
++accepted;
}
printf("accepted=%d\n", accepted);
for (int i = 0; i < 5; ++i) {
printf("rating=%d count=%d\n", i + 1, frequency[i]);
}
return 0;
}| ข้อมูลหกครั้ง | จำนวนที่คาดว่ายอมรับ | ความถี่ระดับ 1–5 ที่ทำนาย | ผลจริง |
|---|---|---|---|
5 4 5 3 4 5 | |||
1 1 1 1 1 1 | |||
0 1 2 3 4 6 | |||
0 6 0 6 0 6 |
ตรวจว่าผลรวมความถี่เท่ากับ accepted และไม่มีการใช้ 0 หรือ 6 เป็นดัชนีก่อนตรวจช่วง จากนั้นหาสองลำดับข้อมูลที่ต่างกันแต่ให้ความถี่เดียวกัน เพื่ออธิบายข้อมูลที่หายไปเมื่อสรุปเป็นความถี่
7.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: อ่านสมาชิกเกินขอบเขตอาร์เรย์
ประเภท: การละเมิดที่ sanitizer ตรวจพบขณะรัน
อาร์เรย์สามสมาชิกมีดัชนี 0, 1, 2 แต่ลูปผิดอ่าน scores[3] ด้วย ซึ่งเป็น undefined behavior ไม่รับรองว่าจะได้เลขขยะหรือ crash ทุกครั้ง ตัวอย่างผิดรันเฉพาะสำเนาที่ติดตั้ง UndefinedBehaviorSanitizer และหยุดเมื่อพบปัญหา
การตรวจเพิ่มเติม: โค้ดผิดรันเฉพาะสำเนาที่สร้างด้วย -fsanitize=undefined -fno-sanitize-recover=all จึงหยุดเมื่อพบการละเมิด ข้อความ runtime error ด้านล่างเป็นของ sanitizer ไม่ใช่ error จาก compiler และการไม่มีข้อความจาก build ธรรมดาไม่ยืนยันว่าเข้าถึงหน่วยความจำถูกต้อง (GCC: Instrumentation Options)
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int main(void)
{
int scores[3] = {70, 80, 90};
for (int i = 0; i <= 3; ++i) {
printf("%d\n", scores[i]);
}
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 -fsanitize=undefined -fno-sanitize-recover=all bad.c -o badไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม
สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ผลจากการรันฉบับผิด
stdout ว่างในการทดลองนี้ หากโปรแกรมหยุดก่อน flush ข้อมูล อาจไม่เห็นข้อความที่คาดว่าจะพิมพ์ก่อนหน้า
stderr:
bad.c:6:30: runtime error: index 3 out of bounds for type 'int [3]'สถานะจบที่ตัวเก็บผลอ่านได้: 1
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
runtime error: index ... out of bounds ... ชี้การเข้าถึงที่เกิดขณะรัน ชนิด int [3] บอกความจุที่ตรวจพบ ต้องเทียบ index กับช่วงที่ถูกต้อง ข้อความนี้มาจาก sanitizer ที่ผู้เขียนเปิด ไม่ใช่สิ่งที่ C รับประกันว่าจะตรวจให้อัตโนมัติทุกแพลตฟอร์ม
โค้ดที่แก้แล้ว
#include <stdio.h>
int main(void)
{
int scores[3] = {70, 80, 90};
for (int i = 0; i < 3; ++i) {
printf("%d\n", scores[i]);
}
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
ใช้เงื่อนไข i < 3 เพื่อไม่สร้างดัชนีเกินขอบเขต แล้วรันฉบับแก้พร้อม sanitizer อีกครั้งเพื่อดูว่าสามค่าครบและไม่มี runtime diagnostic หากฟังก์ชันรับอาร์เรย์ต้องส่งจำนวนข้อมูลที่ถูกต้องด้วย
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
70
80
90ตัวอย่างที่ผิด 2: กำหนดค่าให้อาร์เรย์ทั้งก้อนด้วย assignment
ประเภท: ข้อผิดพลาดขณะคอมไพล์
scores เป็นชื่ออาร์เรย์ ไม่ใช่ตัวแปร int สมาชิกเดียว จึงกำหนด scores = 0 เพื่อทำให้ทุกสมาชิกเป็นศูนย์ไม่ได้ ต้องแยก array initialization ตอนประกาศจาก assignment หลังประกาศ
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int main(void)
{
int scores[3] = {70, 80, 90};
scores = 0;
printf("%d\n", scores[0]);
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:5:12: error: assignment to expression with array type
5 | scores = 0;
| ^สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
assignment to expression with array type บอกว่าด้านซ้ายเป็นอาร์เรย์ซึ่งใช้ assignment แบบนั้นไม่ได้ ไม่ได้หมายความว่าศูนย์มีค่ามากเกินขอบเขต หรือว่าความจุอาร์เรย์เป็นศูนย์
โค้ดที่แก้แล้ว
#include <stdio.h>
int main(void)
{
int scores[3] = {70, 80, 90};
for (int i = 0; i < 3; ++i) {
scores[i] = 0;
}
printf("%d\n", scores[0]);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
กำหนดค่าให้สมาชิกแต่ละตัวด้วยลูปตามความจุ การเขียน scores[0] = 0 อย่างเดียวแก้ได้เฉพาะสมาชิกแรก จึงต้องตรวจสมาชิกอื่นด้วยเมื่อข้อกำหนดคือเคลียร์ทั้งอาร์เรย์
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
0ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 7 อาร์เรย์
ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกประกาศและประมวลผลอาร์เรย์หนึ่งมิติ พร้อมส่งอาร์เรย์เข้าฟังก์ชันเพื่อหาผลรวมและค่าเฉลี่ย ส่วนที่สองฝึกใช้อาร์เรย์สองมิติแสดงตารางด้วยลูปซ้อน ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นการใช้อาร์เรย์ตั้งแต่หนึ่งมิติไปจนถึงสองมิติ
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0
Lab A: อาร์เรย์คะแนน ผลรวมและค่าเฉลี่ย
สถานการณ์ ต้องการเก็บคะแนนของนักศึกษา 5 คนไว้ในอาร์เรย์หนึ่งมิติ แสดงสมาชิกทุกตัว แล้วคำนวณผลรวมและค่าเฉลี่ยด้วยฟังก์ชันที่รับอาร์เรย์ เพื่อฝึกการประกาศอาร์เรย์ การวนประมวลผล และการส่งอาร์เรย์เข้าฟังก์ชัน
เป้าหมาย ฝึก LLO7.1, LLO7.2 และ LLO7.3 คือประกาศและเข้าถึงอาร์เรย์ วนหาผลรวม/ค่าเฉลี่ยโดยไม่เกินขอบเขต และส่งอาร์เรย์เข้าฟังก์ชัน
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch7_array.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch7_array.c : อาร์เรย์หนึ่งมิติ ผลรวม และการส่งอาร์เรย์เข้าฟังก์ชัน */
#include <stdio.h>
int sum_array(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
int main(void) {
int scores[5] = {80, 75, 90, 65, 100};
printf("สมาชิกอาร์เรย์: ");
for (int i = 0; i < 5; i++) {
printf("%d ", scores[i]);
}
printf("\n");
int total = sum_array(scores, 5);
printf("ผลรวม = %d, ค่าเฉลี่ย = %.2f\n", total, (double)total / 5);
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch7_array.c -o ch7_array
./ch7_arrayผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
สมาชิกอาร์เรย์: 80 75 90 65 100
ผลรวม = 410, ค่าเฉลี่ย = 82.00ลูปใน main วนตั้งแต่ดัชนี 0 ถึง 4 ด้วยเงื่อนไข i < 5 จึงแสดงสมาชิกครบทั้งห้าตัวโดยไม่เกินขอบเขต ฟังก์ชัน sum_array รับอาร์เรย์และขนาดไปด้วย แล้ววนบวกสะสมด้วย sum += arr[i]; ส่วนค่าเฉลี่ยคำนวณด้วย (double)total / 5 เพื่อให้ได้ทศนิยม หากหารจำนวนเต็มก่อนจะเสียส่วนทศนิยม ส่วนการใช้ %d กับอาร์กิวเมนต์ double ไม่ใช่การปัดเศษที่ถูกต้อง แต่เป็นพฤติกรรมไม่กำหนดเพราะรูปแบบไม่ตรงชนิด
ตัดสินใจเอง A.1 ต่อยอดโปรแกรมโดยเพิ่มฟังก์ชันที่รับอาร์เรย์และขนาด แล้วคืนค่าคะแนนสูงสุดในอาร์เรย์ โดยวนเทียบทีละสมาชิก ตั้งค่าเริ่มต้นเป็นสมาชิกตัวแรก แล้วปรับค่าสูงสุดเมื่อพบสมาชิกที่มากกว่า พร้อมตั้งชื่อฟังก์ชันและตัวแปรให้สื่อความหมาย
ผลลัพธ์ที่คาดหวัง A.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และแสดงคะแนนสูงสุดได้ถูกต้อง เช่น จากอาร์เรย์ {80, 75, 90, 65, 100} ควรได้คะแนนสูงสุดเท่ากับ 100 (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ A แสดงสมาชิกครบทุกตัว ผลรวมและค่าเฉลี่ยถูกต้อง ดัชนีอยู่ในช่วง 0 ถึง n-1 ไม่เกินขอบเขต ส่งอาร์เรย์เข้าฟังก์ชันพร้อมขนาด และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| โปรแกรมแสดงค่าแปลกหรือพัง | วนเกินขอบเขตด้วยเงื่อนไข i <= 5 จึงเข้าถึง scores[5] | ใช้เงื่อนไข i < 5 ให้ตัวนับน้อยกว่าขนาดอาร์เรย์ |
| ฟังก์ชันคำนวณผลรวมผิด | ลืมส่งขนาดอาร์เรย์เข้าฟังก์ชันหรือส่งขนาดผิด | ส่งขนาดที่ถูกต้องเป็นอาร์กิวเมนต์ที่สองเสมอ |
| ค่าเฉลี่ยไม่มีส่วนทศนิยมที่ต้องการ | หารจำนวนเต็มก่อนแปลงชนิด | ใช้ (double)total / size และแสดงด้วย %.2f |
| รูปแบบแสดงผลไม่ตรงชนิด | ใช้ %d กับค่า double | ใช้ %.2f ให้ตรงชนิด ไม่อาศัยผลของรูปแบบผิด |
เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว
ใบงานตรวจ Lab A: ข้อมูลที่จับข้อผิดพลาดได้
นอกจากชุดเดิม ให้ทดลองชุดศูนย์ทั้งหมด ชุดค่าเท่ากัน และชุดที่ผลรวมหารห้าไม่ลงตัว เพื่อให้แยกผลของการหารจำนวนเต็มจากการแสดงทศนิยมได้จริง
| ชุดข้อมูลห้าค่า | ผลรวมที่ทำนาย | ค่าเฉลี่ยที่ทำนาย | ค่าสูงสุดที่ทำนาย |
|---|---|---|---|
80 75 90 65 100 | |||
0 0 0 0 0 | |||
1 1 1 1 2 | |||
-8 -3 -10 -5 -7 |
ชุดค่าลบเป็นข้อมูลทดสอบทั่วไปของฟังก์ชัน ไม่ใช่คะแนนจริง งาน A.1 ต้องเริ่มค่าสูงสุดจากสมาชิกแรกเมื่อมีข้อมูล ไม่เริ่มจาก 0 เพราะ 0 อาจไม่เคยอยู่ในอาร์เรย์และมากกว่าค่าลบทุกตัว
ฟังก์ชัน sum_array ในบทใช้ขนาดที่ผู้เรียกส่งมาเป็นสัญญา ไม่สามารถตรวจเองได้ว่าความยาวตรงพื้นที่จริง ชุดทดลองนี้ใช้ไม่เกินสิบสมาชิก ค่าละ -100 ถึง 100 เพื่อให้ผลรวมอยู่ใน int แน่นอน
Lab B: อาร์เรย์สองมิติ
สถานการณ์ ต้องการเก็บตารางตัวเลข 2 แถว 3 หลักไว้ในอาร์เรย์สองมิติ แล้วแสดงออกเป็นตารางด้วยลูปซ้อน เพื่อฝึกการประกาศและเข้าถึงอาร์เรย์สองมิติ
เป้าหมาย ฝึก LLO7.3 คือใช้อาร์เรย์สองมิติและเข้าถึงสมาชิกด้วยสองดัชนีผ่านลูปซ้อน
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch7_matrix.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch7_matrix.c : อาร์เรย์สองมิติ */
#include <stdio.h>
int main(void) {
int table[2][3] = { {1, 2, 3}, {4, 5, 6} };
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 3; c++) {
printf("%d ", table[r][c]);
}
printf("\n");
}
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch7_matrix.c -o ch7_matrix
./ch7_matrixผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
1 2 3
4 5 6ลูปนอกวนตามแถวด้วยตัวนับ r ตั้งแต่ 0 ถึง 1 และลูปในวนตามหลักด้วยตัวนับ c ตั้งแต่ 0 ถึง 2 การเข้าถึงสมาชิกใช้สองดัชนี table[r][c] เมื่อจบแต่ละแถวจึงขึ้นบรรทัดใหม่ด้วย printf("\n"); ลำดับการวนแบบนี้แสดงตารางตามแถวจากบนลงล่างและจากซ้ายไปขวาซึ่งตรงกับการจัดเก็บแบบเรียงตามแถว
ตัดสินใจเอง B.1 ดัดแปลงโปรแกรมให้คำนวณและแสดงผลรวมของแต่ละแถว โดยในลูปนอกตั้งตัวแปรสะสมของแถวเริ่มที่ 0 แล้วบวกสมาชิกในลูปในด้วยตัวแปรสะสมจากบทที่ 5 เมื่อจบลูปในให้แสดงผลรวมของแถวนั้น พร้อมตั้งชื่อตัวแปรให้สื่อความหมาย
ผลลัพธ์ที่คาดหวัง B.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และแสดงผลรวมของแต่ละแถวได้ถูกต้อง เช่น แถวแรกรวมได้ 6 และแถวที่สองรวมได้ 15 (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ B อาร์เรย์สองมิติแสดงตารางถูกต้อง ลูปซ้อนวนตามแถวและหลักโดยไม่เกินขอบเขต ไม่สลับแถวกับหลัก คอมไพล์ผ่านไม่มีการแจ้งเตือน และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| ตารางแสดงสลับแถวกับหลัก | สลับลำดับดัชนีเป็น table[c][r] หรือวนลูปสลับกัน | ให้ลูปนอกวนแถว ลูปในวนหลัก และเข้าถึงด้วย table[r][c] |
| โปรแกรมแสดงค่าแปลกหรือพัง | ลูปซ้อนวนเกินขอบเขต เช่น r <= 2 หรือ c <= 3 | ตั้งเงื่อนไขให้ตัวนับน้อยกว่าจำนวนแถว/หลัก คือ r < 2 และ c < 3 |
| ทุกค่าพิมพ์ต่อกันในบรรทัดเดียว | ลืมขึ้นบรรทัดใหม่เมื่อจบแต่ละแถว | ใส่ printf("\n"); ท้ายลูปนอกหลังจบลูปใน |
เปิดหลังผู้เรียนส่งงานแล้ว
ใบงานตรวจ Lab B: พิกัดครบก่อนรวมแถว
เขียนพิกัดของ table[2][3] ตามลำดับการพิมพ์ให้ครบหกคู่ แล้วทำนายว่าจะขึ้นบรรทัดใหม่หลังคู่ใด อย่าสลับขนาดเป็น 3 แถว 2 หลักแต่ยังใช้ข้อมูลและขอบเขตเดิมโดยไม่แก้ให้ครบ
| การทดลอง | สิ่งที่ต้องตรวจ |
|---|---|
| ตารางเดิม 1–6 | ครบสองแถว แถวละสามค่า |
| ทุกช่องเป็น 0 | ยังคงจำนวนช่องและรูปตาราง |
| แถวหนึ่งเป็น 1 อีกแถวเป็น 10 | ผลรวมรายแถวไม่สะสมข้ามแถว |
| งานท้ายบท 3 แถว 2 หลัก | จำนวนพิกัดเท่าเดิมแต่แบ่งแถวต่างกัน |
งาน B.1 ให้ตั้งผลรวมแถวใหม่ที่ต้นแต่ละรอบนอก ผลรวมแถวแรกกับแถวสองต้องตรวจแยกกัน การได้ผลรวมทั้งตารางถูกไม่ได้ยืนยันว่าผลรวมแต่ละแถวถูก
เก็บกวาด
เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น ch7_array และ ch7_matrix) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด
ตรวจความเข้าใจก่อนเริ่มบทที่ 8
ตอบด้วยช่วงดัชนี สถานะตัวแปร หรือเส้นทางการทำงานของโปรแกรมที่ทดลองแล้ว
- ความจุ 10 แต่ count เป็น 5 มีดัชนีข้อมูลจริงใดบ้าง และจะเก็บค่าถัดไปที่ใด
- เมื่อนับได้ศูนย์รายการ เหตุใดจึงไม่อ่านสมาชิกแรกหรือหารด้วย count
- ทำไมค่า 0 ที่ find_first คืนจึงไม่ควรถูกตีความว่าไม่พบ
- การแก้ values[0] กับ count ใน array_update ส่งผลต่างกันอย่างไร
- เหตุใดการนับความถี่ต้องตรวจระดับก่อนคำนวณ rating - 1
- ผลรวมรายแถวและผลรวมรายคอลัมน์เกี่ยวข้องกันอย่างไร
เมื่ออธิบายได้ ให้เตรียมคิดว่าอาร์เรย์อักขระจะบอกจุดจบข้อความอย่างไร แล้วต่อที่ บทที่ 8 การจัดการสตริง โดยแยกความจุพื้นที่จากความยาวข้อความเช่นเดียวกับ count ของบทนี้
สรุป
บทนี้นำการวนซ้ำจากบทที่ 5 และฟังก์ชันจากบทที่ 6 มาใช้กับข้อมูลชุดที่เก็บในอาร์เรย์ เริ่มจากอาร์เรย์หนึ่งมิติที่เก็บข้อมูลชนิดเดียวกันหลายค่าในชื่อเดียว ประกาศด้วย int scores[5]; กำหนดค่าเริ่มต้นด้วยวงเล็บปีกกา และเข้าถึงสมาชิกด้วยดัชนีที่เริ่มจาก 0 โดยอาร์เรย์ขนาด n มีดัชนี 0 ถึง n-1 ต่อด้วยการประมวลผลอาร์เรย์ด้วยลูป for เพื่อหาผลรวมและค่าเฉลี่ย พร้อมระวังความคลาดเคลื่อนที่ขอบเขตด้วยการตั้งเงื่อนไข i < size จากนั้นอธิบายการส่งอาร์เรย์เข้าฟังก์ชันด้วย int sum_array(int arr[], int size) ที่ต้องส่งขนาดไปด้วย และฟังก์ชันทำงานกับอาร์เรย์เดิม ต่างจากการส่งตัวแปรเดี่ยวแบบส่งสำเนาค่าในบทที่ 6 สุดท้ายคืออาร์เรย์สองมิติ int table[2][3] ที่เข้าถึงด้วยสองดัชนี table[r][c] ผ่านลูปซ้อน ทักษะเหล่านี้เป็นพื้นฐานของการจัดการข้อมูลเป็นชุดในบทถัดไป
สิ่งที่ส่งต่อไปยังบทถัดไป
| สิ่งที่ส่งต่อ | ค่า/รูปแบบ | ใช้ในบท |
|---|---|---|
| อาร์เรย์หนึ่งมิติและดัชนีเริ่ม 0 | int a[n];, a[i], ดัชนี 0 ถึง n-1 | บท 8 (สตริง), บท 9 (struct), บท 10 (เรียง/ค้นหา) |
| การวนประมวลผลอาร์เรย์ด้วยลูป | for (int i = 0; i < size; i++), sum += a[i]; | บท 10 (เรียง/ค้นหา) เป็นต้นไป |
| การส่งอาร์เรย์เข้าฟังก์ชัน | int f(int arr[], int size) ส่งขนาดไปด้วย | บท 10 เป็นต้นไป |
| อาร์เรย์สองมิติและลูปซ้อน | int t[r][c], ลูปนอกวนแถว ลูปในวนหลัก | บท 9 (struct), บท 10 (เรียง/ค้นหา) |
สรุปคำสั่ง
| คำสั่ง | หน้าที่ |
|---|---|
gcc -std=c11 -Wall ch7_array.c -o ch7_array | คอมไพล์ซอร์สโค้ด ch7_array.c เป็นไฟล์ปฏิบัติการ ch7_array พร้อมเปิดการแจ้งเตือน |
gcc -std=c11 -Wall ch7_matrix.c -o ch7_matrix | คอมไพล์ซอร์สโค้ด ch7_matrix.c เป็นไฟล์ปฏิบัติการ ch7_matrix พร้อมเปิดการแจ้งเตือน |
./ch7_array หรือ ./ch7_matrix | รันไฟล์ปฏิบัติการที่คอมไพล์แล้ว |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
กลุ่ม ก (เข้าใจ)
- ก1 (LLO7.1) จงอธิบายว่าอาร์เรย์ขนาด n มีดัชนีที่ใช้ได้ตั้งแต่เท่าใดถึงเท่าใด และเหตุใดการเข้าถึงดัชนีที่เกินช่วงนี้จึงเป็นข้อผิดพลาด
- ก2 (LLO7.3) จงอธิบายว่าเหตุใดการส่งอาร์เรย์เข้าฟังก์ชันจึงต้องส่งขนาดของอาร์เรย์ไปพร้อมกันเสมอ
กลุ่ม ข (ประยุกต์)
- ข1 (LLO7.2) จงเขียนโปรแกรมภาษา C ที่ประกาศอาร์เรย์จำนวนเต็ม 6 ตัวพร้อมกำหนดค่าเริ่มต้น แล้วใช้ลูปหาผลรวมและค่าเฉลี่ยโดยไม่เกินขอบเขตดัชนี แสดงค่าเฉลี่ยเป็นทศนิยมสองตำแหน่ง แล้วคอมไพล์และรันให้ได้ผลจริง
- ข2 (LLO7.3) จงเขียนโปรแกรมที่ประกาศอาร์เรย์สองมิติขนาด 3 แถว 2 หลักพร้อมกำหนดค่าเริ่มต้น แล้วใช้ลูปซ้อนแสดงสมาชิกทุกตัวเป็นตาราง
กลุ่ม ค (วิเคราะห์)
- ค1 (LLO7.2) จากโปรแกรมที่วนประมวลผลอาร์เรย์ขนาด 5 ด้วยเงื่อนไขลูป
i <= 5จงวิเคราะห์ว่าเกิดปัญหาใดได้บ้าง และเสนอวิธีแก้เงื่อนไขให้ถูกต้อง
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาอาร์เรย์และการเข้าถึงด้วยดัชนีเพิ่มเติมจาก Kernighan และ Ritchie (1988) บทที่ 1 และบทที่ 5 ว่าด้วยอาร์เรย์ แล้วทดลองเขียนโปรแกรมที่ค้นหาสมาชิกที่มีค่ามากที่สุดในอาร์เรย์
- ทบทวนข้อกำหนดเรื่องอาร์เรย์และขอบเขตดัชนีในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจว่าเหตุใดภาษา C จึงไม่ตรวจขอบเขตดัชนีให้โดยอัตโนมัติ
- ทดลองออกแบบโปรแกรมอาร์เรย์สองมิติที่คำนวณผลรวมของแต่ละแถวและแต่ละหลัก แล้วเปรียบเทียบลำดับการวนลูปซ้อนที่เหมาะสมกับการจัดเก็บแบบเรียงตามแถว
เอกสารอ้างอิง
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 ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว
แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 7
- GNU C: Accessing Array Elements — ดัชนีเริ่มศูนย์และความรับผิดชอบต่อขอบเขต
- GNU C: Initializers — ตัวกำหนดค่าเริ่มต้นและสมาชิกส่วนที่เหลือเป็นศูนย์
- GNU C: Declaring an Array — จำนวนสมาชิกและชนิดของแต่ละช่อง
- GNU C: Array parameters are pointers — การปรับชนิดพารามิเตอร์อาร์เรย์และพื้นที่สมาชิกเดิม
- GNU C: Multidimensional Arrays — อาร์เรย์ของแถวและลำดับจัดเก็บ
ตรวจเอกสารก่อนเขียนส่วนเพิ่มเมื่อ 23 ตุลาคม 2569 ตัวอย่างใช้ hosted C11 และข้อมูลสมมติในช่วงที่ชนิดเก็บได้ ชุดทดสอบไม่อ่านหรือเขียนนอกอาร์เรย์เพื่อคาดหวังอาการตายตัว เวลาในแผนสอนเป็นประมาณการกิจกรรม ไม่ใช่ผลวัดจริง