Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 6 ฟังก์ชันและการออกแบบแบบโมดูล
แผนบริหารการสอนประจำบทที่ 6
บทนี้ว่าด้วยฟังก์ชัน (function) ของภาษา C และการออกแบบโปรแกรมแบบโมดูล ต่อยอดจากโครงสร้างควบคุมแบบวนซ้ำในบทที่ 5 และแบบทางเลือกในบทที่ 4 โดยอธิบายแนวคิดฟังก์ชันในฐานะกลุ่มคำสั่งที่ตั้งชื่อและเรียกใช้ซ้ำได้ การเขียนต้นแบบฟังก์ชัน (prototype) การนิยามและเรียกใช้ฟังก์ชันที่มีพารามิเตอร์และการคืนค่า ฟังก์ชันแบบ void ที่ไม่คืนค่า การส่งค่าแบบ by value ขอบเขตตัวแปรแบบ local และ global และหลักการออกแบบฟังก์ชันให้มีหน้าที่เดียว ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์ เพื่อให้แบ่งโปรแกรมใหญ่เป็นส่วนย่อยที่อ่านง่ายและทดสอบง่าย
| ผลลัพธ์ | ข้อความ | หลักฐาน | CLO |
|---|---|---|---|
| LLO6.1 | อธิบายและเขียนฟังก์ชัน (ต้นแบบ นิยาม พารามิเตอร์ และการคืนค่า) เพื่อแบ่งงานเป็นส่วนย่อย | งานข้อ 1 (ปฏิบัติการที่ 6 Lab A) | CLO2 |
| LLO6.2 | อธิบายการส่งค่าแบบ by value และขอบเขตตัวแปร (local/global) | งานข้อ 2 (ปฏิบัติการที่ 6 Lab B) | CLO2 |
| LLO6.3 | ออกแบบฟังก์ชันตามหลักโมดูล (หนึ่งฟังก์ชันหนึ่งหน้าที่ ตั้งชื่อสื่อความหมาย ใส่คอมเมนต์) | งานข้อ 3 (ปฏิบัติการที่ 6 Lab A/B) | CLO5 |
| LLO6.4 | ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้ | โค้ดทุกข้อที่ส่งในบทนี้ | CLO5 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO2 อธิบายขั้นตอนการทำงาน (Logic) ของภาษาโปรแกรมและเลือกใช้โครงสร้างควบคุม (Control Structure) ให้สอดคล้องกับโจทย์ที่กำหนดได้
- CLO5 ปฏิบัติตามมาตรฐานการเขียนโค้ด (Indentation, Naming, Commenting, Git commits ที่มีข้อความที่สื่อความหมาย)
บทที่ 6 เป็นบทที่ทำให้โปรแกรมแบ่งงานออกเป็นส่วนย่อยที่เรียกใช้ซ้ำได้ จึงเป็นสะพานเชื่อมความรู้เรื่องการวนซ้ำและการเลือกทำงานในบทที่ 4–5 ไปสู่การประมวลผลอาร์เรย์ในบทที่ 7 ซึ่งจะส่งอาร์เรย์เข้าฟังก์ชันต่อไป งานทั้งสามข้อในบทนี้สะสมหลักฐานของ CLO2 (การออกแบบตรรกะและเลือกโครงสร้างควบคุมภายในฟังก์ชัน) และ CLO5 (มาตรฐานการเขียนโค้ด) ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท
วิธีสอน
- ทบทวนลูปและการเลือกทำงานจากบทที่ 4–5 แล้วตั้งโจทย์ที่ต้องเขียนตรรกะชุดเดิมซ้ำในหลายที่ เพื่อชี้ให้เห็นความจำเป็นของการแบ่งงานเป็นฟังก์ชัน
- แนะนำโครงสร้างฟังก์ชันและต้นแบบฟังก์ชัน (prototype) โดยสาธิตการประกาศต้นแบบก่อน
mainและนิยามฟังก์ชันภายหลังบนเครื่องจริง - เปรียบเทียบฟังก์ชันที่คืนค่าด้วย
returnกับฟังก์ชันแบบvoidที่ไม่คืนค่า พร้อมชี้การจับคู่ชนิดพารามิเตอร์กับอาร์กิวเมนต์ - สาธิตการส่งค่าแบบ by value ด้วยการแก้ค่าพารามิเตอร์ภายในฟังก์ชัน แล้วแสดงว่าตัวแปรเดิมของผู้เรียกไม่เปลี่ยน พร้อมอธิบายขอบเขตตัวแปร local และ global แล้วให้ลงมือปฏิบัติตามใบงานโดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
- สรุปหลักการออกแบบโมดูล (หนึ่งฟังก์ชันหนึ่งหน้าที่ ตั้งชื่อสื่อความหมาย ใส่คอมเมนต์) และย้ำข้อผิดพลาดที่พบบ่อย เช่น การลืมต้นแบบ ชนิดคืนค่าไม่ตรง และการเข้าใจผิดว่าการแก้พารามิเตอร์จะเปลี่ยนตัวแปรเดิม
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
- เอกสารประกอบบทที่ 6 และสไลด์สรุปโครงสร้างฟังก์ชัน การส่งค่าแบบ by value และหลักการออกแบบโมดูล
- ไฟล์โปรแกรมตัวอย่าง
ch6_func.cและch6_scope.c
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 7 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 7 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | เขียนฟังก์ชันคืนค่า (max_of, factorial) และฟังก์ชัน void (print_line) พร้อมต้นแบบ แล้วเรียกใช้จาก main | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO6.1 | CLO2 |
| 2 | ทดลองส่งค่าแบบ by value แล้วอธิบายว่าเหตุใดค่าตัวแปรเดิมไม่เปลี่ยน | ไฟล์ซอร์สโค้ด + คำอธิบาย | LLO6.2 | CLO2 |
| 3 | แบ่งโปรแกรมเป็นฟังก์ชันที่มีหน้าที่เดียว ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์ | ไฟล์ซอร์สโค้ด | LLO6.3 | CLO5 |
เงื่อนไขผ่าน: งานข้อ 3 ประเมินจากไฟล์เดียวกับงานข้อ 1 และข้อ 2 ที่ต่อยอดด้วยการจัดโครงสร้างเป็นฟังก์ชันที่มีหน้าที่ชัดเจน ดังนั้นงานข้อ 1 ต้องคอมไพล์ผ่านและให้ผลลัพธ์ถูกต้องก่อน จึงจะประเมินการออกแบบโมดูลในงานข้อ 3 ได้
คะแนนใบงาน (10 คะแนน)
| ผลลัพธ์ | สิ่งที่ตรวจ | คะแนน |
|---|---|---|
| LLO6.1 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO6.2 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO6.3 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO6.4 | มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง | 1 |
ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา
เกณฑ์ให้คะแนน
งานข้อ 1 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:
| เกณฑ์ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| งานข้อ 1 | ฟังก์ชันทุกตัวมีต้นแบบ คอมไพล์ผ่านไม่มีการแจ้งเตือน และผลลัพธ์ตรงทุกค่าทดสอบ | ขาดต้นแบบ ชนิดคืนค่าไม่ตรง หรือผลลัพธ์ไม่ตรงบางค่า |
งานข้อ 2 ใช้รายการตรวจ (checklist): อธิบายได้ว่าฟังก์ชันได้รับสำเนาของอาร์กิวเมนต์, ระบุได้ว่าการแก้ค่าพารามิเตอร์ไม่กระทบตัวแปรเดิมของผู้เรียก, และอธิบายขอบเขตตัวแปร local ได้ถูกต้องจากผลการรันจริง
งานข้อ 3 ใช้รายการตรวจ (checklist): แต่ละฟังก์ชันทำหน้าที่เดียว, ตั้งชื่อฟังก์ชันและพารามิเตอร์สื่อความหมายแบบ snake_case, มีคอมเมนต์ส่วนหัวอธิบายหน้าที่ของฟังก์ชัน, และจัดย่อหน้า 4 ช่องว่าง
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
บทนำ
เมื่อโปรแกรมมีขนาดใหญ่ขึ้น การเขียนคำสั่งทั้งหมดไว้ในฟังก์ชัน main เพียงที่เดียวทำให้โปรแกรมยาว อ่านยาก และแก้ไขลำบาก อีกทั้งตรรกะชุดเดียวกันมักต้องใช้ซ้ำในหลายที่ ฟังก์ชัน (function) จึงเข้ามาช่วยแบ่งงานใหญ่ออกเป็นส่วนย่อยที่ตั้งชื่อและเรียกใช้ซ้ำได้ บทที่ 4–5 ได้แนะนำการเลือกทำงานด้วย if และการวนซ้ำด้วยลูปไปแล้ว บทนี้นำโครงสร้างควบคุมเหล่านั้นมาใช้ภายในฟังก์ชัน พร้อมอธิบายต้นแบบฟังก์ชัน การส่งค่าแบบ by value ขอบเขตตัวแปร และหลักการออกแบบโมดูล โดยยังไม่ใช้อาร์เรย์ซึ่งจะแนะนำในบทที่ 7
ทักษะการออกแบบฟังก์ชันในบทนี้เป็นพื้นฐานของการเขียนโปรแกรมขนาดใหญ่ในบทต่อ ๆ ไป ฟังก์ชันจะถูกใช้ร่วมกับอาร์เรย์ในบทที่ 7 เมื่อเริ่มส่งอาร์เรย์เข้าฟังก์ชัน และเป็นหน่วยพื้นฐานของการทดสอบระดับหน่วยในบทที่ 11 ส่วนหลักการตั้งชื่อและการใส่คอมเมนต์ช่วยยกระดับคุณภาพโค้ดตามมาตรฐาน CLO5 ตลอดรายวิชา
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
| ฟังก์ชัน (ต้นแบบ นิยาม การเรียกใช้) | บท 7 (อาร์เรย์) เป็นต้นไป |
| การส่งค่าแบบ by value และขอบเขตตัวแปร | ทุกบทที่ออกแบบฟังก์ชัน |
| หลักการออกแบบโมดูลและการตั้งชื่อ/คอมเมนต์ | บท 11 (ทดสอบหน่วย) และทุกบทตาม CLO5 |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE และต้องมีความรู้เรื่องการวนซ้ำด้วยลูปจากบทที่ 5 การเลือกทำงานด้วย if จากบทที่ 4 และตัวแปรกับตัวดำเนินการจากบทที่ 2 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ผลการแสดงข้อความภาษาไทยอาจต่างกันเล็กน้อย แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน
วิธีใช้บทเรียนและใบงานในหน้านี้
เริ่มจาก บทที่ 5 โครงสร้างควบคุมแบบวนซ้ำ โดยเลือกกลุ่มคำสั่งที่คำนวณงานหนึ่งได้ครบ แล้วตั้งชื่อให้กลุ่มนั้น บทนี้ยังใช้ลูปและ if เดิม แต่เพิ่มข้อตกลงว่ารับค่าอะไรและส่งผลอะไรกลับ
ใบงานเป็นตารางสำหรับคัดลงสมุดหรือไฟล์ของตน ไม่ใช่แบบฟอร์มส่งคำตอบบนเว็บ ใช้ลำดับ เขียนสัญญา → ทำนาย → trace → คอมไพล์ → รัน → อธิบาย พร้อมเก็บผลก่อนแก้ไว้เปรียบเทียบ
กิจกรรมเพิ่มใช้ LLO6.1–6.3 และงานส่งเดิม ไม่เพิ่มสัดส่วนคะแนน ตัวเลขโบนัสและจำนวนอุปกรณ์เป็นข้อมูลสมมติสำหรับฝึกเขียนโปรแกรม แผนเวลาและแนวคำตอบอยู่ในส่วนผู้สอน
6.1 แนวคิดฟังก์ชันและการแบ่งงานเป็นส่วนย่อย
ระดับที่ 1: ฟังก์ชัน (function) คือกลุ่มคำสั่งที่ตั้งชื่อไว้ เพื่อให้เรียกใช้งานซ้ำได้หลายครั้งโดยอ้างถึงชื่อนั้น แทนการเขียนคำสั่งชุดเดิมซ้ำทุกที่ที่ต้องการ โปรแกรมภาษา C ที่ผ่านมาใช้ฟังก์ชัน main เป็นจุดเริ่มทำงานอยู่แล้ว บทนี้เพิ่มการสร้างฟังก์ชันของตนเองนอกเหนือจาก main
ระดับที่ 2: การแบ่งโปรแกรมเป็นฟังก์ชันย่อยมีประโยชน์สำคัญสามประการ คือ ลดการเขียนโค้ดซ้ำเพราะเขียนตรรกะหนึ่งครั้งแล้วเรียกใช้ได้หลายครั้ง อ่านง่ายขึ้นเพราะชื่อฟังก์ชันบอกหน้าที่ของกลุ่มคำสั่ง และทดสอบง่ายขึ้นเพราะตรวจสอบฟังก์ชันทีละตัวแยกจากส่วนอื่นได้
ระดับที่ 3: การแบ่งปัญหาใหญ่ออกเป็นฟังก์ชันย่อยเรียกว่าการแยกย่อยแบบโมดูล (modular decomposition) แนวทางคือพิจารณาว่าโปรแกรมประกอบด้วยงานย่อยอะไรบ้าง แล้วออกแบบหนึ่งฟังก์ชันต่อหนึ่งงานย่อย เช่น โปรแกรมคิดเกรดอาจแยกเป็นฟังก์ชันรับคะแนน ฟังก์ชันคำนวณเกรด และฟังก์ชันแสดงผล การแยกเช่นนี้ทำให้แก้ไขหรือทดแทนงานย่อยหนึ่งได้โดยไม่กระทบส่วนอื่น
คำถามตรวจความเข้าใจ 6.1: การแบ่งโปรแกรมเป็นฟังก์ชันย่อยมีประโยชน์อย่างไรบ้าง และการแยกย่อยแบบโมดูลหมายถึงอะไร
ใบงาน 1: แบ่งงานตามเหตุผลที่ต้องเปลี่ยน
สมมติต้องรับจำนวนอุปกรณ์และขนาดชุด แล้วรายงานจำนวนชุดเต็มกับของเหลือ หากเปลี่ยนภาษาในข้อความ ควรแก้ส่วนแสดงผล หากเปลี่ยนกติกาจัดชุด ควรแก้ส่วนคำนวณ ไม่จำเป็นต้องย้ายทุกคำสั่งออกจาก main
| งานย่อย | ข้อมูลเข้าพร้อมหน่วย | ผลลัพธ์ | รับหรือพิมพ์ข้อความเองหรือไม่ | ชื่อฟังก์ชันที่เสนอ |
|---|---|---|---|---|
| ตรวจขอบเขตจำนวนอุปกรณ์ | ||||
| คำนวณจำนวนชุดเต็ม | ||||
| คำนวณจำนวนที่เหลือ | ||||
| แสดงรายงาน |
ให้เพื่อนอ่านเฉพาะชื่อและรายการพารามิเตอร์ แล้วบอกได้หรือไม่ว่าต้องเตรียมข้อมูลใด ชื่อ process(a, b) ไม่บอกหน่วยหรือผลลัพธ์ ลองปรับชื่อก่อนเพิ่มคอมเมนต์ยาวเพื่อชดเชยชื่อที่กำกวม
6.2 การนิยามและเรียกใช้ฟังก์ชัน
ระดับที่ 1: การนิยามฟังก์ชันประกอบด้วยชนิดของค่าที่คืน ชื่อฟังก์ชัน พารามิเตอร์ในวงเล็บ และกลุ่มคำสั่งในวงเล็บปีกกา ดังรูปแบบ ชนิดที่คืนค่า ชื่อ(พารามิเตอร์) { คำสั่ง; return ค่า; } เมื่อต้องการใช้งานให้เรียกฟังก์ชันด้วยชื่อตามด้วยอาร์กิวเมนต์ในวงเล็บ
ระดับที่ 2: คอมไพเลอร์ต้องรู้จักฟังก์ชันก่อนจุดที่เรียกใช้ หากนิยามฟังก์ชันไว้หลัง main ต้องประกาศต้นแบบฟังก์ชัน (prototype) ไว้ก่อน main เพื่อบอกชนิดคืนค่าและชนิดพารามิเตอร์ ต้นแบบคือบรรทัดส่วนหัวของฟังก์ชันปิดท้ายด้วยเครื่องหมายอัฒภาค ; ดังตัวอย่าง
#include <stdio.h>
int max_of(int a, int b); /* ต้นแบบฟังก์ชัน */
int main(void) {
int m = max_of(7, 3); /* การเรียกใช้ */
printf("%d\n", m);
return 0;
}
int max_of(int a, int b) { /* การนิยาม */
if (a > b) {
return a;
}
return b;
}ต้นแบบ int max_of(int a, int b); บอกคอมไพเลอร์ว่าฟังก์ชันนี้รับจำนวนเต็มสองตัวและคืนจำนวนเต็ม ส่วนการนิยามที่อยู่หลัง main คือเนื้อหาจริงของฟังก์ชัน
ระดับที่ 3: ชนิดและจำนวนของอาร์กิวเมนต์ที่ส่งเข้าต้องตรงกับพารามิเตอร์ในต้นแบบ หากส่งชนิดไม่ตรง เช่น ส่งค่า double ให้พารามิเตอร์ชนิด int ภาษา C จะแปลงชนิดโดยปริยายและอาจทำให้ค่าคลาดเคลื่อน การจับคู่ชนิดพารามิเตอร์กับอาร์กิวเมนต์ให้ตรงจึงช่วยลดข้อผิดพลาดที่คอมไพเลอร์อาจไม่แจ้งเตือน
คำถามตรวจความเข้าใจ 6.2: ต้นแบบฟังก์ชัน (prototype) มีไว้เพื่ออะไร และต้องวางไว้ที่ตำแหน่งใดของโปรแกรม
ใบงาน 2: เชื่อมต้นแบบ นิยาม และจุดเรียก
ในตัวอย่าง max_of ให้ขีดเส้นใต้ชนิดคืนค่า วงชื่อพารามิเตอร์ และระบุอาร์กิวเมนต์จริงที่จุดเรียก แยกบทบาทของ a กับ 7 และ b กับ 3 แม้ชื่อในต้นแบบจะตั้งต่างจากนิยามได้ ชุดชนิดต้องสอดคล้องกัน
| ส่วนที่เห็น | บอกอะไรกับคอมไพเลอร์หรือผู้เรียก | เริ่มคำนวณแล้วหรือยัง |
|---|---|---|
int max_of(int a, int b); | ||
int m = max_of(7, 3); | ||
เนื้อหานิยาม max_of |
ต้นแบบที่มี ; ยังไม่ทำงานตามเนื้อหาฟังก์ชัน คำสั่งเรียกจึงเป็นจุดเริ่มส่งค่าและรับผล สำหรับ C11 ใช้ name(void) เมื่อไม่รับอาร์กิวเมนต์ ไม่ใช้ name() เพื่อสื่อข้อตกลงไม่มีพารามิเตอร์
ทดลองในสำเนาโดยส่งอาร์กิวเมนต์เพียงหนึ่งตัวให้ max_of แล้วอ่านคำวินิจฉัยจากคอมไพเลอร์ แก้ให้ครบก่อนรัน การคอมไพล์ไม่ผ่านเป็นผลที่ตั้งใจของกิจกรรมนี้ ไม่ใช่โปรแกรมตัวอย่างที่ควรนำไปรัน
6.3 การส่งค่าและการคืนค่า
ระดับที่ 1: พารามิเตอร์คือช่องทางรับค่าเข้าสู่ฟังก์ชัน ส่วนคำสั่ง return คือช่องทางส่งผลลัพธ์ออกจากฟังก์ชันกลับไปยังจุดที่เรียก เมื่อฟังก์ชันพบคำสั่ง return จะหยุดทำงานทันทีและส่งค่าที่ระบุกลับไป
ระดับที่ 2: ฟังก์ชันบางตัวทำงานโดยไม่ต้องคืนค่า เช่น ฟังก์ชันที่พิมพ์ข้อความออกหน้าจอ ฟังก์ชันแบบนี้ประกาศชนิดคืนค่าเป็น void และไม่มีคำสั่ง return ส่งค่า (หรือใช้ return; เปล่า ๆ เพื่อออกจากฟังก์ชัน) ตารางด้านล่างเปรียบเทียบฟังก์ชันสองแบบ
| ลักษณะ | ฟังก์ชันคืนค่า | ฟังก์ชันแบบ void |
|---|---|---|
| ชนิดคืนค่า | เช่น int, double | void |
คำสั่ง return | ส่งค่ากลับ เช่น return a; | ไม่ส่งค่า (อาจใช้ return; เปล่า) |
| การนำผลไปใช้ | นำค่าที่คืนไปใช้ต่อได้ เช่น กำหนดให้ตัวแปร | เรียกเพื่อให้เกิดผลข้างเคียง เช่น พิมพ์ข้อความ |
ระดับที่ 3: การส่งอาร์กิวเมนต์เข้าฟังก์ชันในภาษา C เป็นการส่งค่าแบบ by value หมายความว่าฟังก์ชันได้รับ "สำเนา" ของค่าอาร์กิวเมนต์ ไม่ใช่ตัวแปรเดิม การแก้ค่าพารามิเตอร์ภายในฟังก์ชันจึงเปลี่ยนเฉพาะสำเนา ไม่กระทบตัวแปรเดิมของผู้เรียก ลักษณะนี้ยืนยันได้จากผลการรันจริงใน Lab B ของบทนี้ ที่ค่าในฟังก์ชันเปลี่ยนเป็น 999 แต่ตัวแปรเดิมยังคงเป็น 10 หากต้องการนำผลที่ฟังก์ชันคำนวณกลับไปใช้ ให้ใช้การคืนค่าด้วย return แทนการพยายามแก้ค่าพารามิเตอร์
คำถามตรวจความเข้าใจ 6.3: เหตุใดการแก้ค่าพารามิเตอร์ภายในฟังก์ชันจึงไม่กระทบตัวแปรของผู้เรียก และหากต้องการนำผลกลับไปใช้ควรทำอย่างไร
ใบงาน 3: เรียกสำเร็จไม่ได้แปลว่าเก็บผลแล้ว
สร้างไฟล์ returned_value.c เพื่อเปรียบเทียบการไม่ใช้ค่าคืน การเก็บในตัวแปรใหม่ และการกำหนดกลับตัวแปรเดิม ใช้ช่วง -100 ถึง 100 เพื่อให้การเพิ่ม 5 สองครั้งอยู่ในช่วง int แน่นอน
/* returned_value.c : ignored, stored, assigned, and nested results */
#include <stdio.h>
int add_bonus(int score);
int main(void)
{
int score;
if (scanf("%d", &score) != 1) {
puts("Input error");
return 1;
}
if (score < -100 || score > 100) {
puts("Range error");
return 1;
}
add_bonus(score);
printf("ignored=%d\n", score);
int adjusted = add_bonus(score);
printf("stored original=%d adjusted=%d\n", score, adjusted);
int twice = add_bonus(add_bonus(score));
printf("nested=%d\n", twice);
score = add_bonus(score);
printf("assigned=%d\n", score);
return 0;
}
/* Return an adjusted copy; do not change the caller's variable. */
int add_bonus(int score)
{
score += 5;
return score;
}| ข้อมูลเข้า | score หลังเรียกแบบไม่เก็บ | adjusted | twice | score หลังรับผลกลับ |
|---|---|---|---|---|
| 0 | ||||
| 70 | ||||
| -5 |
trace การเรียกซ้อนเป็นสองครั้ง โดยให้การเรียกด้านในคืนผลก่อนแล้วจึงใช้ค่านั้นเป็นอาร์กิวเมนต์ด้านนอก ไม่ต้องนำกฎนี้ไปเดาลำดับประเมินอาร์กิวเมนต์อิสระหลายตัวของฟังก์ชันเดียว ซึ่งเป็นอีกประเด็นหนึ่ง
6.4 ขอบเขตตัวแปรและการออกแบบโมดูล
ระดับที่ 1: ตัวแปรที่ประกาศในบล็อกของฟังก์ชันเป็นตัวแปรเฉพาะที่ (local) ใช้ชื่อได้ภายในขอบเขตตั้งแต่จุดประกาศ ตัวอย่างนี้ใช้ local แบบอัตโนมัติซึ่งหมดอายุเมื่อออกจากบล็อกของมัน ตัวแปรชื่อเดียวกันในคนละฟังก์ชันจึงเป็นคนละตัวและไม่เกี่ยวข้องกัน
ระดับที่ 2: ตรงข้ามกับตัวแปร local คือตัวแปรส่วนกลาง (global) ที่ประกาศนอกฟังก์ชัน โดยชื่อระดับไฟล์มองเห็นได้ตั้งแต่จุดประกาศไปในขอบเขตของมัน แม้ตัวแปร global จะสะดวก แต่ควรหลีกเลี่ยงเพราะทุกฟังก์ชันแก้ค่าได้ ทำให้ติดตามที่มาของค่าและหาสาเหตุข้อผิดพลาดได้ยาก แนวทางที่ดีกว่าคือส่งค่าที่ต้องใช้ผ่านพารามิเตอร์และส่งผลกลับด้วย return เพื่อให้แต่ละฟังก์ชันพึ่งพาเฉพาะข้อมูลที่รับเข้ามา
ระดับที่ 3: หลักการออกแบบโมดูลที่สำคัญคือ หนึ่งฟังก์ชันควรทำหน้าที่เดียวที่ชัดเจน เพราะฟังก์ชันที่ทำหลายหน้าที่จะอ่านยาก ทดสอบยาก และนำกลับมาใช้ซ้ำได้ยากกว่า นอกจากนี้ควรตั้งชื่อฟังก์ชันและพารามิเตอร์ให้สื่อความหมายแบบ snake_case และใส่คอมเมนต์ส่วนหัวอธิบายหน้าที่ของฟังก์ชัน การปฏิบัติเช่นนี้สอดคล้องกับมาตรฐานการเขียนโค้ดตาม CLO5 และทำให้โปรแกรมบำรุงรักษาได้ง่ายขึ้น
คำถามตรวจความเข้าใจ 6.4: เหตุใดจึงควรหลีกเลี่ยงตัวแปร global และเหตุใดฟังก์ชันที่ทำหลายหน้าที่จึงด้อยกว่าฟังก์ชันที่ทำหน้าที่เดียว
6.5 กรณีศึกษาต่อเนื่อง: แยกกติกาการคำนวณออกจากหน้าจอ
เมื่อโปรแกรมจัดชุดอุปกรณ์มีการรับข้อมูล ตรวจขอบเขต คำนวณ และแสดงผล การใส่ทุกอย่างใน main ยังทำงานได้ แต่ทำให้ทดสอบสูตรโดยไม่ป้อนข้อมูลซ้ำยากขึ้น การแยกฟังก์ชันควรเริ่มจากหน้าที่ที่อธิบายได้หนึ่งเรื่อง ไม่ใช่ตัดโค้ดทุกห้าบรรทัดออกเป็นชื่อใหม่
กำหนดฟังก์ชัน complete_kits ว่ารับปริมาณและขนาดชุด คืนจำนวนชุดสมบูรณ์ และคืน -1 เมื่อข้อมูลไม่อยู่ในขอบเขต ตัวอย่างนี้ใช้ค่าที่ไม่ติดลบและตัวหารบวก จึงแยก -1 ออกจากคำตอบปกติได้ การเลือกค่าพิเศษต้องมีสัญญาชัด ไม่ใช่ทุกฟังก์ชันใช้ -1 แทนข้อผิดพลาดได้เสมอ
#include <stdio.h>
int complete_kits(int quantity, int per_kit)
{
if (quantity < 0 || per_kit <= 0) {
return -1;
}
return quantity / per_kit;
}
int main(void)
{
printf("%d\n", complete_kits(17, 3));
printf("%d\n", complete_kits(2, 3));
printf("%d\n", complete_kits(17, 0));
return 0;
}ผลคือ 5, 0 และ -1 คนละบรรทัด กรณีได้ 0 เป็นคำตอบปกติของของไม่พอหนึ่งชุด ส่วน -1 หมายถึงไม่ได้คำนวณตามข้อตกลง ผู้เรียกต้องตรวจผลนี้ก่อนนำไปแสดงเป็นจำนวนชุด หากพิมพ์ว่า “จัดได้ -1 ชุด” แม้ฟังก์ชันทำตามสัญญา แต่ส่วนแสดงผลยังตีความผิด
6.5.1 เขียนสัญญาฟังก์ชันก่อนเขียนเนื้อหา
| ส่วนของสัญญา | คำถามที่ต้องตอบ | ตัวอย่าง |
|---|---|---|
| ข้อมูลเข้า | ต้องมีค่าอะไรและหน่วยใด | ปริมาณเป็นชิ้น ขนาดชุดเป็นชิ้นต่อชุด |
| ขอบเขต | ยอมรับค่าใด | ปริมาณไม่ติดลบ ขนาดชุดบวก |
| ผลคืน | หมายความว่าอะไร | จำนวนชุดสมบูรณ์ หรือ -1 |
| ผลข้างเคียง | เปลี่ยนข้อมูลหรือแสดงข้อความหรือไม่ | ตัวอย่างนี้ไม่แสดงและไม่แก้ตัวแปรผู้เรียก |
| กรณีขอบเขต | ต้องทดลองอะไร | ของศูนย์ ของน้อยกว่าชุด และขนาดชุดศูนย์ |
ฟังก์ชันที่คำนวณอย่างเดียวสามารถเรียกจากหน้าจอหลายแบบและทดสอบด้วยค่าคงที่ได้ง่ายกว่า ถ้าฝัง scanf ไว้ในฟังก์ชันคำนวณทุกตัว การทดสอบจะผูกกับการป้อนข้อความ และผู้เรียกไม่สามารถควบคุมข้อมูลเข้าได้ตรง ๆ จึงควรแยกการรับข้อมูลกับการคำนวณเมื่อทำให้หน้าที่ชัดขึ้น
ใบงาน 4: ค่าพิเศษต้องแยกจากคำตอบปกติ
ก่อนขยาย complete_kits ให้เติมตารางจากสัญญาของฟังก์ชัน ห้ามนำค่าคืนไปพิมพ์เป็นจำนวนชุดทันทีโดยไม่แยกกรณีผิดพลาด
| quantity | per_kit | ยอมรับหรือไม่ | ค่าคืนที่คาด | เหตุผล |
|---|---|---|---|---|
| 0 | 3 | |||
| 2 | 3 | |||
| 17 | 3 | |||
| 18 | 3 | |||
| 17 | 0 | |||
| -1 | 3 |
ค่า 0 หมายถึงจัดชุดเต็มไม่ได้ แต่ยังเป็นข้อมูลถูกต้อง ส่วน -1 เป็นสัญญาณข้อมูลผิด เพราะคำตอบปกติไม่ติดลบ การออกแบบนี้ใช้ได้กับสัญญานี้ ไม่ใช่กฎว่า -1 เป็นข้อผิดพลาดในทุกงาน
ให้เขียนกรณีที่คำตอบปกติอาจติดลบ เช่น ผลต่างอุณหภูมิ แล้วอภิปรายว่าทำไมการใช้ -1 อย่างเดิมจึงกำกวม โดยยังไม่ต้องออกแบบโครงสร้างข้อมูลใหม่ก่อนเรียนถึงเรื่องนั้น
ตัวอย่างพร้อมใช้: ให้ผู้เรียกตีความค่าคืน
สร้างไฟล์ kit_report.c โดยแยกสูตรจำนวนชุดกับของเหลือออกจากข้อความรายงาน รับ quantity ช่วง 0–1000 และ per_kit ช่วง 1–100 เพื่อให้ชุดฝึกมีขอบเขตชัด ฟังก์ชันคำนวณคืน -1 เมื่อผิดสัญญา
/* kit_report.c : calculate values separately from presentation */
#include <stdio.h>
int complete_kits(int quantity, int per_kit);
int leftover_items(int quantity, int per_kit);
void print_report(int kits, int leftover);
int main(void)
{
int quantity;
int per_kit;
if (scanf("%d %d", &quantity, &per_kit) != 2) {
puts("Input error");
return 1;
}
int kits = complete_kits(quantity, per_kit);
int leftover = leftover_items(quantity, per_kit);
if (kits == -1 || leftover == -1) {
puts("Range error");
return 1;
}
print_report(kits, leftover);
return 0;
}
/* Return complete kits, or -1 for an invalid training input. */
int complete_kits(int quantity, int per_kit)
{
if (quantity < 0 || quantity > 1000 || per_kit < 1 || per_kit > 100) {
return -1;
}
return quantity / per_kit;
}
/* Return unused items under the same input contract. */
int leftover_items(int quantity, int per_kit)
{
if (quantity < 0 || quantity > 1000 || per_kit < 1 || per_kit > 100) {
return -1;
}
return quantity % per_kit;
}
/* Display already validated calculation results. */
void print_report(int kits, int leftover)
{
printf("kits=%d leftover=%d\n", kits, leftover);
}โปรแกรมเต็มนี้กำหนดช่วงฝึกแคบกว่าตัวอย่าง complete_kits ก่อนหน้า ให้ระบุว่าใช้สัญญาของฉบับใดเมื่อทดสอบ ทั้งสองฉบับไม่ทำการหารเมื่อข้อมูลไม่ผ่าน และไม่ตีความ -1 ว่าเป็นจำนวนชุด
ทดสอบคู่ 17/3, 18/3, 2/3 และ 0/3 แล้วตรวจความสัมพันธ์ kits * per_kit + leftover == quantity เฉพาะข้อมูลที่ยอมรับ ส่วนค่าผิดให้ตรวจว่าแสดงข้อผิดพลาดโดยไม่เรียกส่วนรายงานผลปกติ
scanf ที่อ่านสำเร็จสองค่าไม่ได้รับรองว่าข้อความทั้งบรรทัดมีแต่สองจำนวน เช่น 17 3x อาจอ่านได้สองค่าและเหลือ x ใช้ข้อมูลตัวเลขที่แทนด้วย int ได้ ไม่ทดสอบเลขยาวเกินชนิดเพื่อหวังผลที่แน่นอน
6.6 ส่งค่า คืนค่า และอายุของตัวแปรผ่าน trace
6.6.1 สำเนาที่มีชื่อเหมือนกันไม่ใช่ตัวแปรเดียวกัน
#include <stdio.h>
int add_bonus(int score)
{
score += 5;
return score;
}
int main(void)
{
int score = 70;
int adjusted = add_bonus(score);
printf("original=%d adjusted=%d\n", score, adjusted);
return 0;
}ผลคือ original=70 adjusted=75 ตัวแปร score ใน main กับพารามิเตอร์ score อยู่คนละขอบเขต เมื่อเรียกฟังก์ชัน ค่าถูกส่งไปตั้งต้นพารามิเตอร์ การแก้พารามิเตอร์จึงไม่แก้ตัวแปรต้นฉบับโดยตรง จากนั้น return ส่งค่า 75 กลับมาให้ผู้เรียกเลือกเก็บใน adjusted
หากเปลี่ยนการเรียกเป็น add_bonus(score); โดยไม่เก็บผล ค่าใน main ยังเป็น 70 การเรียกฟังก์ชันสำเร็จไม่ได้แปลว่าตัวแปรเดิมจะเปลี่ยนตามชื่อฟังก์ชัน หากต้องการเปลี่ยนค่าเดิมอย่างชัดเจนในแบบ pass-by-value ใช้ score = add_bonus(score);
GNU C: Function Call Semantics อธิบายการส่งสำเนาค่าและการคืนผล ตัวอย่างในบทนี้ใช้ int main(void) เพื่อยึดรูปแบบโปรแกรม hosted C11 ที่เรียนในรายวิชา แม้เอกสารภาษา GNU บางตัวอย่างจะใช้รูปแบบเฉพาะอื่น
6.6.2 ไล่การเรียกซ้อนโดยแยกกรอบตัวแปร
หากเขียน add_bonus(add_bonus(70)) การเรียกด้านในคืน 75 แล้วค่าจึงเป็นอาร์กิวเมนต์ของการเรียกด้านนอก ซึ่งคืน 80 ให้ trace เป็นสองกรอบ อย่าจินตนาการว่าพารามิเตอร์เดิมยังค้างรอเพิ่มซ้ำ แต่ละครั้งมีการทำงานและตัวแปรของตนเอง
ในขั้นต้นควรใช้ตัวแปรชั่วคราวชื่อสื่อความหมายแทนการซ้อนหลายชั้น เช่น first_adjustment และ second_adjustment เพื่อให้ตรวจเหตุผลได้ง่าย การย่อให้สั้นลงไม่จำเป็นต้องทำให้คุณภาพดีขึ้น โดยเฉพาะเมื่อต้องอธิบายความต่างระหว่างค่าเดิมกับค่าที่ปรับแล้ว
6.6.3 ขอบเขตไม่เท่ากับการแบ่งหน้าที่ที่ดี
การย้ายตัวแปรทั้งหมดไปเป็น global ทำให้ทุกฟังก์ชันมองเห็นได้ แต่เพิ่มความสัมพันธ์ที่ต้องจำ ฟังก์ชันชื่อ calculate_total อาจเปลี่ยนยอดที่ฟังก์ชันอื่นใช้โดยผู้เรียกมองไม่เห็น การส่งข้อมูลที่ต้องใช้เป็นพารามิเตอร์ช่วยให้สัญญาปรากฏตรงจุดเรียก และช่วยแยกทดสอบแต่ละส่วน
ตัวแปร local ที่ยังไม่ได้ตั้งค่าไม่ควรถูกอ่านเพียงเพราะเพิ่งประกาศ ชื่อที่อยู่ในขอบเขตหมายถึงเข้าถึงชื่อนั้นได้ ไม่ได้รับรองว่ามีค่าที่มีความหมายแล้ว ต้องตรวจทั้งการประกาศ การกำหนดค่า และทุกเส้นทางก่อนนำไปใช้
ใบงาน 5: ชื่อเหมือนกันอยู่คนละขอบเขต
สร้างไฟล์ scope_frames.c เพื่อเห็นทั้งตัวแปรระดับไฟล์ ตัวแปรในฟังก์ชัน และตัวแปรในบล็อกย่อย ตัวอย่างนี้จงใจใช้ชื่อ total ซ้ำเพื่อศึกษา เมื่อเขียนงานจริงควรเลือกชื่อที่ติดตามหน้าที่ได้ง่าย
/* scope_frames.c : file scope, parameter scope, and block scope */
#include <stdio.h>
int total = 100;
int next_value(int total);
void show_total(void);
int main(void)
{
int value = 10;
int result = next_value(value);
printf("caller value=%d result=%d\n", value, result);
show_total();
total += 1;
show_total();
return 0;
}
/* This parameter is distinct from the file-scope total. */
int next_value(int total)
{
{
int total = 7;
printf("block=%d\n", total);
}
printf("parameter=%d\n", total);
return total + 1;
}
/* Read the file-scope total; this function has no parameters. */
void show_total(void)
{
printf("file=%d\n", total);
}| จุดที่อ่านค่า | ชื่อที่เขียน | หมายถึงตัวแปรใด | ค่าที่ทำนาย |
|---|---|---|---|
| ในบล็อกย่อยของ next_value | total | ||
| หลังออกจากบล็อกย่อย | total | ||
| ใน main หลังคืนค่า | value และ result | ||
| เรียก show_total ครั้งแรก | total | ||
| เรียก show_total ครั้งที่สอง | total |
การเห็นชื่อได้ขึ้นกับขอบเขตจากจุดประกาศ ไม่ใช่เพียงการมีชื่ออยู่ที่ใดสักแห่งในไฟล์ ตัวแปรในบล็อกย่อยบังชื่อด้านนอกเฉพาะภายในบล็อกนั้น พอออกมาจะกลับไปอ้างถึงพารามิเตอร์
ตัวอย่างนี้ใช้ตัวแปร local อัตโนมัติ ซึ่งหมดอายุเมื่อออกจากบล็อกของมัน อย่าสรุปว่าตัวแปร local ทุกชนิดมีอายุเท่ากัน เพราะตัวแปร local แบบ static มีอายุจัดเก็บต่างกันและอยู่นอกกิจกรรมนี้
6.7 เวิร์กช็อปขยาย: ออกแบบสองแบบแล้วเปรียบเทียบ
นำโปรแกรมยอดซื้อสมมติมาแบ่งเป็นส่วนคำนวณยอดก่อนส่วนลด คำนวณส่วนลดตามกติกาแบบฝึกหัด และแสดงรายงาน เขียนรายชื่อฟังก์ชันกับสัญญาก่อนลงรายละเอียด แล้วให้เพื่อนลองบอกข้อมูลที่ต้องส่งเข้าจากชื่อและพารามิเตอร์ หากฟังก์ชันชื่อ process รับตัวเลขห้าค่าโดยไม่รู้หน่วย ให้ปรับชื่อก่อนเพิ่มโค้ด
สร้างฉบับแรกที่ทุกฟังก์ชันใช้ global และฉบับที่สองที่ส่งค่าผ่านพารามิเตอร์ ทดลองเรียกส่วนคำนวณสองครั้งด้วยข้อมูลต่างกันโดยไม่รับ input เพิ่ม สังเกตว่าฉบับใดแยกผลของแต่ละครั้งได้ง่ายกว่า จุดประสงค์ไม่ใช่ประกาศว่า global ผิดทุกกรณี แต่เห็นต้นทุนของข้อมูลที่เปลี่ยนได้จากหลายจุด
ต่อยอดฟังก์ชันจัดชุดด้วยฟังก์ชันหาของเหลือ แยกกรณีข้อมูลผิดอย่างสอดคล้องกัน ทดสอบ 17 กับ 3, 18 กับ 3, 2 กับ 3 และ 0 กับ 3 จากนั้นตรวจว่าจำนวนชุดคูณขนาดชุดบวกของเหลือเท่าปริมาณเดิมในทุกกรณีที่ยอมรับ โดยใช้ค่าขนาดเล็กที่ไม่เสี่ยง overflow
หลักฐานที่ต้องอธิบายได้คือชื่อพารามิเตอร์กับตัวแปรผู้เรียกเป็นคนละตัว ค่าคืนถูกเก็บที่ใด และฟังก์ชันใดมีหน้าที่แสดงผล เมื่อโปรแกรมใหญ่ขึ้นในบทโครงงาน สัญญาเหล่านี้จะช่วยให้เปลี่ยนส่วนแสดงผลโดยไม่ต้องแก้สูตรคำนวณทุกจุด
ใบงานออกแบบ: เปลี่ยนข้อความโดยไม่เปลี่ยนสูตร
ใช้โปรแกรมจัดชุดเป็นฐาน สร้างสำเนาที่เปลี่ยนข้อความจาก kits และ leftover เป็นภาษาไทย โดยเก็บฟังก์ชันคำนวณและข้อมูลทดสอบเดิม เปรียบเทียบว่าค่าที่ได้ยังเท่ากันหรือไม่
| สิ่งที่จะเปลี่ยน | ฟังก์ชันที่ควรแก้ | ส่วนที่ควรทดสอบซ้ำ | เหตุผล |
|---|---|---|---|
| ภาษาในรายงาน | |||
| ขอบเขต quantity ที่ยอมรับ | |||
| สูตรจำนวนชุด | |||
| รูปแบบอ่านสองจำนวน |
หากทั้งสูตรและข้อความกระจายอยู่ทุกฟังก์ชัน ให้เริ่มจัดหน้าที่ใหม่ก่อนเพิ่มฟังก์ชันอีกตัว การแยกโมดูลไม่ได้วัดจากจำนวนชื่อฟังก์ชัน แต่วัดว่าตามข้อมูลเข้า ผลลัพธ์ และเหตุผลที่ต้องแก้ไขได้หรือไม่
6.8 ตัวอย่างเปรียบเทียบ: ฟังก์ชันที่คืนค่ากับฟังก์ชันที่แสดงค่า
สมมติต้องการหาพื้นที่สี่เหลี่ยมแบบจำลองจากด้าน 4 และ 6 ฟังก์ชันหนึ่งอาจคำนวณแล้วคืน 24 อีกฟังก์ชันอาจคำนวณแล้วพิมพ์ 24 แต่ไม่คืนผลให้ผู้เรียก สองฟังก์ชันทำให้คนเห็นตัวเลขเดียวกันบนหน้าจอได้ แต่ใช้ต่อในโปรแกรมต่างกัน
ถ้าต้องรวมพื้นที่หลายชิ้น ฟังก์ชันที่คืนค่าเหมาะกับการนำไปบวกต่อโดยไม่ต้องอ่านข้อความจากหน้าจอกลับมา ส่วนฟังก์ชันแสดงผลเหมาะกับจุดที่ต้องสื่อสารกับผู้ใช้ ควรตั้งชื่อให้เห็นความต่าง เช่น rectangle_area และ print_area ไม่ใช้ชื่อเดียวกันในคำอธิบายจนสับสน
trace การทำงานเป็นสามขั้น ผู้เรียกเตรียมข้อมูล ฟังก์ชันสร้างค่าผลลัพธ์ และผู้เรียกนำผลไปใช้ คำสั่ง return จบการทำงานของฟังก์ชันนั้นทันที คำสั่งหลัง return บนเส้นทางเดียวกันจะไม่ทำงาน จึงไม่ควรวางข้อความสรุปสำคัญหลังจุดคืนค่าแล้วสงสัยว่าทำไมไม่แสดง
| วัตถุประสงค์ | ข้อมูลเข้า | สิ่งที่ต้องส่งกลับ | ควรพิมพ์เองหรือไม่ |
|---|---|---|---|
| หาพื้นที่ | กว้างและยาว | ค่าพื้นที่ | ไม่จำเป็น |
| รวมพื้นที่ | ค่าพื้นที่ย่อย | ผลรวม | ไม่จำเป็น |
| แสดงรายงาน | ค่าที่คำนวณแล้ว | อาจไม่มีค่าผลลัพธ์ | เป็นหน้าที่หลัก |
ในการฝึก ให้เขียนฟังก์ชันแปลงวินาทีเป็นจำนวนชั่วโมงเต็มที่รับค่าและคืนค่า จากนั้นเรียกทั้งในข้อความรายงานและในการทดสอบแบบค่าคงที่ จะเห็นประโยชน์ของการแยกสูตรออกจากรูปแบบหน้าจอ ลองเปลี่ยนภาษาในข้อความโดยไม่แก้ฟังก์ชันคำนวณและตรวจว่าคำตอบยังเดิม
เมื่อแบ่งงานเป็นหลายฟังก์ชัน ให้ระวังการแยกที่ทำให้ข้อมูลต้องวิ่งอ้อมโดยไม่มีประโยชน์ ฟังก์ชันที่เพียงเรียกฟังก์ชันอื่นด้วยชื่อไม่สื่อความอาจเพิ่มภาระการอ่าน การแบ่งที่ดีช่วยให้แต่ละส่วนมีสัญญา ตรวจได้ และเปลี่ยนเหตุผลหนึ่งเรื่องได้โดยไม่กระทบทุกส่วน
กรณีศึกษาเสริม: คืนชั่วโมงเต็มแล้วนำไปใช้ต่อ
สร้างไฟล์ hours_report.c รับวินาทีช่วง 0–30000 แล้วแยกเป็นชั่วโมงเต็มกับวินาทีที่เหลือในชั่วโมง ใช้จำนวนเต็มทั้งหมด ไม่ปัดเศษเวลา และให้ส่วนแสดงผลรับค่าที่คำนวณแล้ว
/* hours_report.c : return values before formatting output */
#include <stdio.h>
int whole_hours(int seconds);
int remaining_seconds(int seconds);
void print_duration(int hours, int remainder);
int main(void)
{
int seconds;
if (scanf("%d", &seconds) != 1) {
puts("Input error");
return 1;
}
int hours = whole_hours(seconds);
int remainder = remaining_seconds(seconds);
if (hours == -1 || remainder == -1) {
puts("Range error");
return 1;
}
print_duration(hours, remainder);
return 0;
}
/* Return full hours for the training domain, or -1. */
int whole_hours(int seconds)
{
if (seconds < 0 || seconds > 30000) {
return -1;
}
return seconds / 3600;
}
/* Return seconds left within the last hour, or -1. */
int remaining_seconds(int seconds)
{
if (seconds < 0 || seconds > 30000) {
return -1;
}
return seconds % 3600;
}
/* Print calculated parts; do not read input or change them. */
void print_duration(int hours, int remainder)
{
printf("hours=%d remainder=%d\n", hours, remainder);
}ทำนายเมื่อรับ 0, 3599, 3600, 3601 และ 7200 แล้วตรวจ hours * 3600 + remainder == seconds ส่วนที่เหลือมีหน่วยวินาที ไม่ใช่ตัวเลขนาทีที่นำไปต่อหลังจุดทศนิยมของชั่วโมงได้ทันที
หากฟังก์ชันคำนวณพิมพ์อย่างเดียวโดยไม่คืนค่า ผู้เรียกจะนำผลไปตรวจความสัมพันธ์นี้โดยตรงไม่ได้ ให้ชี้บรรทัดที่ทำหน้าที่คืนค่ากับบรรทัดที่แสดงค่า แยกออกจากกันก่อนเปลี่ยนรูปแบบรายงาน
6.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: เรียกฟังก์ชันโดยส่งอาร์กิวเมนต์ไม่ครบ
ประเภท: ข้อผิดพลาดขณะคอมไพล์
นิยาม add กำหนดพารามิเตอร์สองตัวแต่จุดเรียกส่งเพียงหนึ่งค่า จึงไม่ตรงสัญญาของฟังก์ชัน ไม่ใช่การที่ right ได้ค่า 0 ให้อัตโนมัติ และภาษา C ไม่มี default argument ในรูปแบบนี้
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int add(int left, int right)
{
return left + right;
}
int main(void)
{
printf("%d\n", add(3));
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:8:20: error: too few arguments to function 'add'
8 | printf("%d\n", add(3));
| ^~~
bad.c:2:5: note: declared here
2 | int add(int left, int right)
| ^~~สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
too few arguments to function ระบุชื่อฟังก์ชันที่รับค่าไม่ครบ note declared here ชี้ declaration ที่ compiler ใช้ตรวจ ให้เทียบจุดเรียกกับรายการพารามิเตอร์พร้อมกัน
โค้ดที่แก้แล้ว
#include <stdio.h>
int add(int left, int right)
{
return left + right;
}
int main(void)
{
printf("%d\n", add(3, 4));
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
ส่ง 3 และ 4 ให้ครบตาม signature แล้วตรวจผลรวม 7 หากโจทย์ต้องการฟังก์ชันพารามิเตอร์เดียวจริงต้องออกแบบนิยามและจุดเรียกใหม่ให้สอดคล้อง ไม่ลบพารามิเตอร์เพียงเพื่อหยุด error
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
7ตัวอย่างที่ผิด 2: ประกาศฟังก์ชันแต่ไม่มีนิยามให้ linker
ประเภท: ข้อผิดพลาดขณะเชื่อมโยงโปรแกรม
prototype บอกชื่อ ชนิดคืนค่า และพารามิเตอร์ ทำให้ตรวจจุดเรียกได้ แต่ยังไม่มี implementation เมื่อต้องเชื่อมโปรแกรมจริง linker จึงหา add ไม่พบ ถ้ามีนิยามอยู่ไฟล์อื่นต้องนำไฟล์หรือ object นั้นเข้าขั้น link ด้วย
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int add(int left, int right);
int main(void)
{
printf("%d\n", add(3, 4));
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad/usr/bin/ld: /tmp/ccJ5XIN3.o: in function `main':
bad.c:(.text+0x13): undefined reference to `add'
collect2: error: ld returned 1 exit statusสถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
undefined reference เป็นข้อความจากขั้นเชื่อมโยง ไม่ใช่ undeclared ของ compiler บรรทัด collect2 ... ld returned ... สรุปว่าการ link ล้ม ส่วนชื่อ add บอกสัญลักษณ์ที่ยังขาด ชื่อไฟล์ชั่วคราวของ object เปลี่ยนได้ระหว่างการทดลอง
โค้ดที่แก้แล้ว
#include <stdio.h>
int add(int left, int right)
{
return left + right;
}
int main(void)
{
printf("%d\n", add(3, 4));
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
เพิ่มนิยาม add ตามตัวอย่างหรือเชื่อมไฟล์ที่นิยามฟังก์ชันไว้ให้ครบ การเพิ่ม prototype ซ้ำไม่สร้างตัวฟังก์ชันและไม่แก้ undefined reference
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
7ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 6 ฟังก์ชันและการออกแบบแบบโมดูล
ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกเขียนฟังก์ชันคืนค่าและฟังก์ชันแบบ void พร้อมต้นแบบ แล้วเรียกใช้จาก main ส่วนที่สองฝึกสังเกตการส่งค่าแบบ by value และขอบเขตตัวแปร ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นการใช้ฟังก์ชันตั้งแต่โครงสร้างพื้นฐานไปจนถึงการส่งค่าและขอบเขตตัวแปร
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0
Lab A: ฟังก์ชันคืนค่าและ void
สถานการณ์ ต้องการสร้างฟังก์ชันสามตัว คือ max_of ที่คืนค่ามากกว่าของจำนวนเต็มสองจำนวน factorial ที่คืนค่าแฟกทอเรียลของจำนวนเต็ม และ print_line ที่เป็นฟังก์ชันแบบ void สำหรับพิมพ์เส้นขีด แล้วเรียกใช้ทั้งสามจาก main เพื่อฝึกเขียนต้นแบบ การนิยาม และการเรียกฟังก์ชัน
เป้าหมาย ฝึก LLO6.1 และ LLO6.3 คือเขียนฟังก์ชันคืนค่าและ void พร้อมต้นแบบ และออกแบบฟังก์ชันให้มีหน้าที่เดียว ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch6_func.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch6_func.c : ฟังก์ชันคืนค่า ฟังก์ชันแบบ void และขอบเขตตัวแปร */
#include <stdio.h>
int max_of(int a, int b);
int factorial(int n);
void print_line(int length);
int main(void) {
printf("max_of(7, 3) = %d\n", max_of(7, 3));
printf("factorial(5) = %d\n", factorial(5));
print_line(10);
return 0;
}
int max_of(int a, int b) {
if (a > b) {
return a;
}
return b;
}
int factorial(int n) {
if (n < 0 || n > 7) {
return -1;
}
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
void print_line(int length) {
for (int i = 0; i < length; i++) {
printf("-");
}
printf("\n");
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch6_func.c -o ch6_func
./ch6_funcผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
max_of(7, 3) = 7
factorial(5) = 120
----------ฟังก์ชัน max_of และ factorial คืนค่าจำนวนเต็มด้วย return จึงนำผลไปแสดงด้วย printf ได้ ส่วน print_line เป็นฟังก์ชันแบบ void ที่เรียกเพื่อให้เกิดผลคือพิมพ์เส้นขีดตามจำนวนที่กำหนด โดยไม่คืนค่า สังเกตว่าฟังก์ชันทั้งสามมีต้นแบบประกาศไว้ก่อน main และตัวแปร result กับ i เป็นตัวแปร local ที่ใช้ได้เฉพาะในฟังก์ชันของตน
ตัดสินใจเอง A.1 เพิ่มฟังก์ชัน min_of ของตนเองที่คืนค่าน้อยกว่าของจำนวนเต็มสองจำนวน โดยประกาศต้นแบบไว้ก่อน main ใส่คอมเมนต์ส่วนหัวอธิบายหน้าที่ และเรียกใช้จาก main เพื่อแสดงผล
ผลลัพธ์ที่คาดหวัง A.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และแสดงค่าน้อยกว่าของจำนวนสองจำนวนได้ถูกต้อง เช่น min_of(7, 3) ควรได้ 3 (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ A ฟังก์ชันทุกตัวมีต้นแบบประกาศก่อน main คอมไพล์ผ่านไม่มีการแจ้งเตือน ผลลัพธ์ตรงทุกค่าทดสอบ แต่ละฟังก์ชันทำหน้าที่เดียว ตั้งชื่อสื่อความหมาย และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| คอมไพเลอร์แจ้งเตือนว่าไม่รู้จักฟังก์ชัน | ลืมประกาศต้นแบบก่อน main | เพิ่มต้นแบบของฟังก์ชันไว้ก่อน main |
| ค่าที่คืนกลับผิดชนิดหรือเพี้ยน | ชนิดคืนค่าไม่ตรงกับที่ประกาศ | ตรวจให้ชนิดคืนค่าของต้นแบบ นิยาม และค่าที่ return ตรงกัน |
| ฟังก์ชันคืนค่าแต่ไม่มีผล | ลืมคำสั่ง return หรือไม่นำค่าที่คืนไปใช้ | เพิ่ม return และนำค่าที่คืนไปแสดงหรือกำหนดให้ตัวแปร |
เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว
ใบงานตรวจ Lab A: ข้อตกลงและขอบเขตผลคำนวณ
ฟังก์ชัน factorial ในฉบับนี้ยอมรับ 0–7 และคืน -1 เมื่ออยู่นอกช่วง เพราะ 7! เท่ากับ 5040 อยู่ในช่วงขั้นต่ำของ int ส่วน 8! เท่ากับ 40320 ไม่ได้รับประกันว่าจะใส่ int ได้บนทุกระบบ C11
คำเรียกเดิม factorial(5) ยังคงเดิม อย่าขยายการรับค่าโดยอาศัยเพียงว่าคอมพิวเตอร์เครื่องหนึ่งยังคำนวณได้ ต้องปรับสัญญา ชนิดข้อมูล และวิธีป้องกัน overflow ร่วมกัน
| ฟังก์ชัน | ข้อมูลที่จะใช้ตรวจ | สิ่งที่ต้องบันทึก |
|---|---|---|
| max_of | 7/3, 3/7, 4/4, -7/-3 | อาร์กิวเมนต์และค่าคืน |
| factorial | 0, 1, 5, 7, -1, 8 | ค่าคืนและกรณีปฏิเสธ |
| print_line | 0, 1, 10 ในช่วงฝึก 0–20 | จำนวนขีดและการขึ้นบรรทัด |
| min_of ใน A.1 | คู่เดิมและคู่ที่สลับตำแหน่ง | ผลต้องไม่ขึ้นกับลำดับสำหรับคู่เดียวกัน |
print_line(0) ยังพิมพ์ขึ้นบรรทัดใหม่ แม้ไม่มีขีดให้พิมพ์ จึงต้องเปรียบเทียบผลทั้งบรรทัด ไม่สรุปว่า void หมายถึง “ไม่ทำอะไร” หรือ “ไม่มีผลลัพธ์ให้สังเกต”
Lab B: by value และขอบเขตตัวแปร
สถานการณ์ ต้องการทดลองเขียนฟังก์ชันที่แก้ค่าพารามิเตอร์ภายในฟังก์ชัน แล้วสังเกตว่าตัวแปรเดิมในฟังก์ชันหลักไม่เปลี่ยน เพื่อเข้าใจการส่งค่าแบบ by value และขอบเขตตัวแปร
เป้าหมาย ฝึก LLO6.2 คืออธิบายการส่งค่าแบบ by value และขอบเขตตัวแปร (local/global) ได้ถูกต้องจากผลการรันจริง
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch6_scope.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch6_scope.c : การส่งค่าแบบ by value และขอบเขตตัวแปร */
#include <stdio.h>
void try_change(int x) {
x = 999;
printf("ภายในฟังก์ชัน x = %d\n", x);
}
int main(void) {
int value = 10;
try_change(value);
printf("ในฟังก์ชันหลัก value = %d\n", value);
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch6_scope.c -o ch6_scope
./ch6_scopeผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
ภายในฟังก์ชัน x = 999
ในฟังก์ชันหลัก value = 10ฟังก์ชัน try_change รับพารามิเตอร์ x ซึ่งเป็นสำเนาของค่า value ที่ส่งเข้ามา เมื่อแก้ x เป็น 999 ค่าภายในฟังก์ชันจึงเป็น 999 แต่ตัวแปร value ในฟังก์ชันหลักยังคงเป็น 10 เพราะภาษา C ส่งค่าแบบ by value ฟังก์ชันได้รับเพียงสำเนา ไม่ใช่ตัวแปรเดิม นอกจากนี้ x เป็นตัวแปร local ของ try_change ส่วน value เป็นตัวแปร local ของ main ทั้งสองเป็นคนละตัวแม้จะมีค่าเริ่มต้นสัมพันธ์กัน
ตัดสินใจเอง B.1 แทนที่จะพยายามแก้ค่าพารามิเตอร์ ให้เขียนฟังก์ชัน doubled ที่รับจำนวนเต็มหนึ่งตัวแล้วคืนค่าที่เป็นสองเท่าด้วย return จากนั้นเรียกใช้จาก main แล้วนำค่าที่คืนไปกำหนดให้ตัวแปรใหม่ เพื่อแสดงว่าการนำผลกลับไปใช้ควรทำผ่านการคืนค่า
ผลลัพธ์ที่คาดหวัง B.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และแสดงค่าสองเท่าได้ถูกต้อง เช่น เมื่อส่งค่า 10 ควรได้ 20 (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ B อธิบายได้ว่าฟังก์ชันได้รับสำเนาของอาร์กิวเมนต์ ระบุได้ว่าการแก้ค่าพารามิเตอร์ไม่กระทบตัวแปรเดิม อธิบายขอบเขตตัวแปร local ได้ถูกต้องจากผลการรันจริง และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| เข้าใจผิดว่าตัวแปรเดิมจะเปลี่ยนตาม | เข้าใจว่าพารามิเตอร์คือตัวแปรเดิม | ทบทวนว่าการส่งค่าแบบ by value ส่งเพียงสำเนา |
| ต้องการผลกลับแต่ค่าไม่เปลี่ยน | พยายามแก้ค่าพารามิเตอร์แทนการคืนค่า | ใช้ return คืนผล แล้วนำค่าที่คืนไปกำหนดให้ตัวแปร |
| ใช้ตัวแปรข้ามฟังก์ชันไม่ได้ | ตัวแปร local มองเห็นเฉพาะในฟังก์ชันของตน | ส่งค่าผ่านพารามิเตอร์หรือคืนค่าด้วย return |
เปิดหลังผู้เรียนส่งงานแล้ว
ใบงานตรวจ Lab B: เก็บหลักฐานก่อนและหลังเรียก
ทำนาย value ก่อนเรียก ค่า x ภายในฟังก์ชัน และ value หลังคืนจากฟังก์ชัน แล้วค่อยรัน เปลี่ยนค่าเริ่มของผู้เรียกเป็น 0, 10 และ -5 โดยไม่แก้คำสั่งที่ตั้ง x = 999
| ค่าเริ่มของ value | x หลังถูกแก้ใน try_change | value หลังเรียก | เหตุผล |
|---|---|---|---|
| 0 | |||
| 10 | |||
| -5 |
สำหรับ B.1 ให้กำหนดช่วงฝึก doubled เป็น -100 ถึง 100 เพื่อให้ผลสองเท่าอยู่ใน int แน่นอน ทดสอบค่าลบ ศูนย์ และค่าบวก โดยยังต้องแสดงว่าค่าเดิมของผู้เรียกอยู่ที่ใดและค่าคืนถูกเก็บที่ใด
งาน sum_to ท้ายบทให้ใช้ n ช่วง 1–100 ในการทดลองและระบุเป็นสัญญา ผลสูงสุด 5050 อยู่ในช่วงขั้นต่ำของ int ไม่ให้ค่าผิดช่วงผ่านไปเข้าลูป และไม่ขยายช่วงโดยยังใช้ชนิดเดิมอย่างไร้ขอบเขต
เก็บกวาด
เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น ch6_func และ ch6_scope) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด
ตรวจความเข้าใจก่อนเริ่มบทที่ 7
ตอบด้วยค่าตัวแปรหรือหน้าที่ของคำสั่งจากโปรแกรม ไม่ใช้เพียงการท่องชื่อส่วนประกอบ
- ต้นแบบกับนิยามทำหน้าที่ต่างกันอย่างไร และคำสั่งเรียกอยู่ที่ไหน
- เมื่อ
add_bonus(score)คืนค่าแต่ไม่ได้เก็บ score ของผู้เรียกเปลี่ยนหรือไม่ - การกำหนด
score = add_bonus(score)เปลี่ยนค่าด้วยคำสั่งใดในผู้เรียก - เหตุใดจำนวนชุด 0 กับค่าผิดพลาด -1 ต้องแสดงความหมายต่างกัน
- เมื่อออกจากบล็อกย่อย ชื่อที่เคยบังพารามิเตอร์ยังถูกอ้างถึงหรือไม่
- เหตุใดฟังก์ชันที่พิมพ์ค่าอย่างเดียวจึงใช้ต่อแตกต่างจากฟังก์ชันคืนค่า
เมื่ออธิบายได้ ให้เตรียมคิดว่าถ้ามีข้อมูลหลายค่าจะจัดเก็บและส่งเข้าไปคำนวณอย่างไร แล้วต่อที่ บทที่ 7 อาร์เรย์ โดยยังไม่สรุปว่าการส่งอาร์เรย์ทำงานเหมือนสำเนาจำนวนเต็มทุกประการ
สรุป
บทนี้ต่อยอดโครงสร้างควบคุมจากบทที่ 4–5 ไปสู่การแบ่งโปรแกรมเป็นฟังก์ชันย่อย เริ่มจากแนวคิดฟังก์ชันในฐานะกลุ่มคำสั่งที่ตั้งชื่อและเรียกใช้ซ้ำได้ และประโยชน์สามประการคือลดการเขียนซ้ำ อ่านง่าย และทดสอบง่าย ต่อด้วยโครงสร้างการนิยามฟังก์ชัน การประกาศต้นแบบ (prototype) ก่อน main และการจับคู่ชนิดพารามิเตอร์กับอาร์กิวเมนต์ จากนั้นอธิบายพารามิเตอร์ที่รับค่าเข้า คำสั่ง return ที่ส่งผลออก ฟังก์ชันแบบ void ที่ไม่คืนค่า และการส่งค่าแบบ by value ที่ฟังก์ชันได้รับเพียงสำเนา ซึ่งยืนยันจากผลการรันจริงว่าตัวแปรเดิมของผู้เรียกไม่เปลี่ยน สุดท้ายคือขอบเขตตัวแปร local และ global เหตุผลที่ควรเลี่ยง global และหลักการออกแบบโมดูล หนึ่งฟังก์ชันหนึ่งหน้าที่ ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์ ทักษะเหล่านี้เป็นพื้นฐานของการเขียนโปรแกรมขนาดใหญ่และการทดสอบระดับหน่วยในบทต่อไป
สิ่งที่ส่งต่อไปยังบทถัดไป
| สิ่งที่ส่งต่อ | ค่า/รูปแบบ | ใช้ในบท |
|---|---|---|
| ฟังก์ชัน (ต้นแบบ นิยาม การเรียกใช้) | ชนิดคืนค่า ชื่อ(พารามิเตอร์) { ... return ...; }, ต้นแบบก่อน main | บท 7 (อาร์เรย์) เป็นต้นไป |
การส่งค่าแบบ by value และ return | ฟังก์ชันได้รับสำเนาของอาร์กิวเมนต์; คืนผลด้วย return | ทุกบทที่ออกแบบฟังก์ชัน |
ฟังก์ชันแบบ void และขอบเขตตัวแปร | void ไม่คืนค่า; ตัวแปร local เห็นเฉพาะในฟังก์ชัน | บท 7 เป็นต้นไป |
| หลักการออกแบบโมดูล | หนึ่งฟังก์ชันหนึ่งหน้าที่ ตั้งชื่อ snake_case ใส่คอมเมนต์ | บท 11 (ทดสอบหน่วย) และทุกบทตาม CLO5 |
สรุปคำสั่ง
| คำสั่ง | หน้าที่ |
|---|---|
gcc -std=c11 -Wall ch6_func.c -o ch6_func | คอมไพล์ซอร์สโค้ด ch6_func.c เป็นไฟล์ปฏิบัติการ ch6_func พร้อมเปิดการแจ้งเตือน |
gcc -std=c11 -Wall ch6_scope.c -o ch6_scope | คอมไพล์ซอร์สโค้ด ch6_scope.c เป็นไฟล์ปฏิบัติการ ch6_scope พร้อมเปิดการแจ้งเตือน |
./ch6_func หรือ ./ch6_scope | รันไฟล์ปฏิบัติการที่คอมไพล์แล้ว |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
กลุ่ม ก (เข้าใจ)
- ก1 (LLO6.1) จงอธิบายส่วนประกอบของการนิยามฟังก์ชัน และบอกว่าต้นแบบฟังก์ชัน (prototype) มีไว้เพื่ออะไรและต้องวางไว้ที่ใด
- ก2 (LLO6.2) จงอธิบายความหมายของการส่งค่าแบบ by value และเหตุใดการแก้ค่าพารามิเตอร์ภายในฟังก์ชันจึงไม่กระทบตัวแปรของผู้เรียก
กลุ่ม ข (ประยุกต์)
- ข1 (LLO6.1) จงเขียนโปรแกรมภาษา C ที่มีฟังก์ชัน
sum_toรับจำนวนเต็มบวก n แล้วคืนผลรวมของจำนวน 1 ถึง n ด้วยลูป พร้อมประกาศต้นแบบและเรียกใช้จากmainแล้วคอมไพล์และรันให้ได้ผลจริง - ข2 (LLO6.3) จงแบ่งโปรแกรมที่รับจำนวนเต็มสองจำนวนแล้วแสดงค่ามากกว่าและค่าน้อยกว่า ออกเป็นฟังก์ชันที่มีหน้าที่เดียว ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์ส่วนหัวของแต่ละฟังก์ชัน
กลุ่ม ค (วิเคราะห์)
- ค1 (LLO6.2) จากโปรแกรมที่พยายามให้ฟังก์ชันแก้ค่าตัวแปรของผู้เรียกโดยตรงด้วยพารามิเตอร์แบบ by value แล้วพบว่าค่าเดิมไม่เปลี่ยน จงวิเคราะห์สาเหตุและเสนอวิธีแก้ให้โปรแกรมนำผลที่ฟังก์ชันคำนวณกลับไปใช้ได้
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาเรื่องฟังก์ชัน การประกาศต้นแบบ และการส่งอาร์กิวเมนต์เพิ่มเติมจาก Kernighan และ Ritchie (1988) บทที่ 4 ว่าด้วยฟังก์ชันและโครงสร้างโปรแกรม แล้วทดลองแยกโปรแกรมที่เขียนไว้ในบทก่อนออกเป็นฟังก์ชันย่อย
- ทบทวนความหมายของการส่งค่าแบบ by value และขอบเขตตัวแปรในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจกฎการมองเห็นและอายุของตัวแปรภายในฟังก์ชัน
- ทดลองออกแบบโปรแกรมหนึ่งเรื่องโดยแบ่งเป็นฟังก์ชันที่มีหน้าที่เดียวหลายตัว แล้วเปรียบเทียบความอ่านง่ายและความสะดวกในการทดสอบกับการเขียนทั้งหมดไว้ใน
main
เอกสารอ้างอิง
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 ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว
แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 6
- GNU C: Function Definitions — ส่วนหัวและเนื้อหาฟังก์ชัน
- GNU C: Function Declarations — ต้นแบบและการประกาศก่อนใช้งาน
- GNU C: Function Call Semantics — สำเนาค่าอาร์กิวเมนต์และผลคืน
- GNU C: Scope — ขอบเขตชื่อและการบังชื่อ
ตรวจเอกสารก่อนเรียบเรียงเมื่อ 23 ตุลาคม 2569 ตัวอย่างใช้ hosted C11 และ int main(void) ไม่ใช้รูปแบบ GNU เฉพาะบางตัวอย่าง กรณีทดลองคอมไพล์ผิดใช้เพื่ออ่านคำวินิจฉัย ไม่รันโค้ดที่ยังผิด