Sign in

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

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

บทที่ 6 ฟังก์ชันและการออกแบบแบบโมดูล

ShareFacebookXLinkedInLINE

แผนบริหารการสอนประจำบทที่ 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 (มาตรฐานการเขียนโค้ด) ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท

วิธีสอน

  1. ทบทวนลูปและการเลือกทำงานจากบทที่ 4–5 แล้วตั้งโจทย์ที่ต้องเขียนตรรกะชุดเดิมซ้ำในหลายที่ เพื่อชี้ให้เห็นความจำเป็นของการแบ่งงานเป็นฟังก์ชัน
  2. แนะนำโครงสร้างฟังก์ชันและต้นแบบฟังก์ชัน (prototype) โดยสาธิตการประกาศต้นแบบก่อน main และนิยามฟังก์ชันภายหลังบนเครื่องจริง
  3. เปรียบเทียบฟังก์ชันที่คืนค่าด้วย return กับฟังก์ชันแบบ void ที่ไม่คืนค่า พร้อมชี้การจับคู่ชนิดพารามิเตอร์กับอาร์กิวเมนต์
  4. สาธิตการส่งค่าแบบ by value ด้วยการแก้ค่าพารามิเตอร์ภายในฟังก์ชัน แล้วแสดงว่าตัวแปรเดิมของผู้เรียกไม่เปลี่ยน พร้อมอธิบายขอบเขตตัวแปร local และ global แล้วให้ลงมือปฏิบัติตามใบงานโดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
  5. สรุปหลักการออกแบบโมดูล (หนึ่งฟังก์ชันหนึ่งหน้าที่ ตั้งชื่อสื่อความหมาย ใส่คอมเมนต์) และย้ำข้อผิดพลาดที่พบบ่อย เช่น การลืมต้นแบบ ชนิดคืนค่าไม่ตรง และการเข้าใจผิดว่าการแก้พารามิเตอร์จะเปลี่ยนตัวแปรเดิม

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

  • เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา 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.1CLO2
2ทดลองส่งค่าแบบ by value แล้วอธิบายว่าเหตุใดค่าตัวแปรเดิมไม่เปลี่ยนไฟล์ซอร์สโค้ด + คำอธิบายLLO6.2CLO2
3แบ่งโปรแกรมเป็นฟังก์ชันที่มีหน้าที่เดียว ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์ไฟล์ซอร์สโค้ดLLO6.3CLO5

เงื่อนไขผ่าน: งานข้อ 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 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง

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

โค้ดตัวอย่างในบทนี้ทดสอบด้วย 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 เพื่อบอกชนิดคืนค่าและชนิดพารามิเตอร์ ต้นแบบคือบรรทัดส่วนหัวของฟังก์ชันปิดท้ายด้วยเครื่องหมายอัฒภาค ; ดังตัวอย่าง

C
#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, doublevoid
คำสั่ง returnส่งค่ากลับ เช่น return a;ไม่ส่งค่า (อาจใช้ return; เปล่า)
การนำผลไปใช้นำค่าที่คืนไปใช้ต่อได้ เช่น กำหนดให้ตัวแปรเรียกเพื่อให้เกิดผลข้างเคียง เช่น พิมพ์ข้อความ

ระดับที่ 3: การส่งอาร์กิวเมนต์เข้าฟังก์ชันในภาษา C เป็นการส่งค่าแบบ by value หมายความว่าฟังก์ชันได้รับ "สำเนา" ของค่าอาร์กิวเมนต์ ไม่ใช่ตัวแปรเดิม การแก้ค่าพารามิเตอร์ภายในฟังก์ชันจึงเปลี่ยนเฉพาะสำเนา ไม่กระทบตัวแปรเดิมของผู้เรียก ลักษณะนี้ยืนยันได้จากผลการรันจริงใน Lab B ของบทนี้ ที่ค่าในฟังก์ชันเปลี่ยนเป็น 999 แต่ตัวแปรเดิมยังคงเป็น 10 หากต้องการนำผลที่ฟังก์ชันคำนวณกลับไปใช้ ให้ใช้การคืนค่าด้วย return แทนการพยายามแก้ค่าพารามิเตอร์

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

ใบงาน 3: เรียกสำเร็จไม่ได้แปลว่าเก็บผลแล้ว

สร้างไฟล์ returned_value.c เพื่อเปรียบเทียบการไม่ใช้ค่าคืน การเก็บในตัวแปรใหม่ และการกำหนดกลับตัวแปรเดิม ใช้ช่วง -100 ถึง 100 เพื่อให้การเพิ่ม 5 สองครั้งอยู่ในช่วง int แน่นอน

C
/* 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 หลังเรียกแบบไม่เก็บadjustedtwicescore หลังรับผลกลับ
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 แทนข้อผิดพลาดได้เสมอ

C
#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 ให้เติมตารางจากสัญญาของฟังก์ชัน ห้ามนำค่าคืนไปพิมพ์เป็นจำนวนชุดทันทีโดยไม่แยกกรณีผิดพลาด

quantityper_kitยอมรับหรือไม่ค่าคืนที่คาดเหตุผล
03
23
173
183
170
-13

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

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

ตัวอย่างพร้อมใช้: ให้ผู้เรียกตีความค่าคืน

สร้างไฟล์ kit_report.c โดยแยกสูตรจำนวนชุดกับของเหลือออกจากข้อความรายงาน รับ quantity ช่วง 0–1000 และ per_kit ช่วง 1–100 เพื่อให้ชุดฝึกมีขอบเขตชัด ฟังก์ชันคำนวณคืน -1 เมื่อผิดสัญญา

C
/* 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 สำเนาที่มีชื่อเหมือนกันไม่ใช่ตัวแปรเดียวกัน

C
#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 ซ้ำเพื่อศึกษา เมื่อเขียนงานจริงควรเลือกชื่อที่ติดตามหน้าที่ได้ง่าย

C
/* 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_valuetotal
หลังออกจากบล็อกย่อย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 แล้วแยกเป็นชั่วโมงเต็มกับวินาทีที่เหลือในชั่วโมง ใช้จำนวนเต็มทั้งหมด ไม่ปัดเศษเวลา และให้ส่วนแสดงผลรับค่าที่คำนวณแล้ว

C
/* 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 และตัวเลือกดังนี้ ชื่อไฟล์ เลขบรรทัด คอลัมน์ ถ้อยคำ และคำเตือนที่เปิดใช้อาจต่างจากแพลตฟอร์มการเรียน นักศึกษาไม่จำเป็นต้องเปลี่ยนเครื่องมือหรือการติดตั้งตามชุดทดสอบของผู้เขียน

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

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

ตัวอย่างที่ผิด 1: เรียกฟังก์ชันโดยส่งอาร์กิวเมนต์ไม่ครบ

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

นิยาม add กำหนดพารามิเตอร์สองตัวแต่จุดเรียกส่งเพียงหนึ่งค่า จึงไม่ตรงสัญญาของฟังก์ชัน ไม่ใช่การที่ right ได้ค่า 0 ให้อัตโนมัติ และภาษา C ไม่มี default argument ในรูปแบบนี้

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

C
#include <stdio.h>
int add(int left, int right)
{
    return left + right;
}
int main(void)
{
    printf("%d\n", add(3));
    return 0;
}

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

TEXT
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad
TEXT
bad.c: In function 'main':
bad.c: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 ใช้ตรวจ ให้เทียบจุดเรียกกับรายการพารามิเตอร์พร้อมกัน

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

C
#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 ว่าง

TEXT
7

ตัวอย่างที่ผิด 2: ประกาศฟังก์ชันแต่ไม่มีนิยามให้ linker

ประเภท: ข้อผิดพลาดขณะเชื่อมโยงโปรแกรม

prototype บอกชื่อ ชนิดคืนค่า และพารามิเตอร์ ทำให้ตรวจจุดเรียกได้ แต่ยังไม่มี implementation เมื่อต้องเชื่อมโปรแกรมจริง linker จึงหา add ไม่พบ ถ้ามีนิยามอยู่ไฟล์อื่นต้องนำไฟล์หรือ object นั้นเข้าขั้น link ด้วย

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

C
#include <stdio.h>
int add(int left, int right);
int main(void)
{
    printf("%d\n", add(3, 4));
    return 0;
}

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

TEXT
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad
TEXT
/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 เปลี่ยนได้ระหว่างการทดลอง

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

C
#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 ว่าง

TEXT
7

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

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

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

ปฏิบัติการที่ 6 ฟังก์ชันและการออกแบบแบบโมดูล

ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกเขียนฟังก์ชันคืนค่าและฟังก์ชันแบบ void พร้อมต้นแบบ แล้วเรียกใช้จาก main ส่วนที่สองฝึกสังเกตการส่งค่าแบบ by value และขอบเขตตัวแปร ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นการใช้ฟังก์ชันตั้งแต่โครงสร้างพื้นฐานไปจนถึงการส่งค่าและขอบเขตตัวแปร

ℹ️Note

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

Lab A: ฟังก์ชันคืนค่าและ void

สถานการณ์ ต้องการสร้างฟังก์ชันสามตัว คือ max_of ที่คืนค่ามากกว่าของจำนวนเต็มสองจำนวน factorial ที่คืนค่าแฟกทอเรียลของจำนวนเต็ม และ print_line ที่เป็นฟังก์ชันแบบ void สำหรับพิมพ์เส้นขีด แล้วเรียกใช้ทั้งสามจาก main เพื่อฝึกเขียนต้นแบบ การนิยาม และการเรียกฟังก์ชัน

เป้าหมาย ฝึก LLO6.1 และ LLO6.3 คือเขียนฟังก์ชันคืนค่าและ void พร้อมต้นแบบ และออกแบบฟังก์ชันให้มีหน้าที่เดียว ตั้งชื่อสื่อความหมาย และใส่คอมเมนต์

ขั้นปฏิบัติ

  1. สร้างไฟล์ชื่อ ch6_func.c ด้วยโปรแกรมแก้ไขข้อความหรือ IDE
  2. พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
C
/* 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");
}
  1. คอมไพล์และรันด้วยคำสั่ง
BASH
gcc -std=c11 -Wall ch6_func.c -o ch6_func
./ch6_func

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

TEXT
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_of7/3, 3/7, 4/4, -7/-3อาร์กิวเมนต์และค่าคืน
factorial0, 1, 5, 7, -1, 8ค่าคืนและกรณีปฏิเสธ
print_line0, 1, 10 ในช่วงฝึก 0–20จำนวนขีดและการขึ้นบรรทัด
min_of ใน A.1คู่เดิมและคู่ที่สลับตำแหน่งผลต้องไม่ขึ้นกับลำดับสำหรับคู่เดียวกัน

print_line(0) ยังพิมพ์ขึ้นบรรทัดใหม่ แม้ไม่มีขีดให้พิมพ์ จึงต้องเปรียบเทียบผลทั้งบรรทัด ไม่สรุปว่า void หมายถึง “ไม่ทำอะไร” หรือ “ไม่มีผลลัพธ์ให้สังเกต”

Lab B: by value และขอบเขตตัวแปร

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

เป้าหมาย ฝึก LLO6.2 คืออธิบายการส่งค่าแบบ by value และขอบเขตตัวแปร (local/global) ได้ถูกต้องจากผลการรันจริง

ขั้นปฏิบัติ

  1. สร้างไฟล์ชื่อ ch6_scope.c ด้วยโปรแกรมแก้ไขข้อความหรือ IDE
  2. พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
C
/* 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;
}
  1. คอมไพล์และรันด้วยคำสั่ง
BASH
gcc -std=c11 -Wall ch6_scope.c -o ch6_scope
./ch6_scope

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

TEXT
ภายในฟังก์ชัน 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

ค่าเริ่มของ valuex หลังถูกแก้ใน try_changevalue หลังเรียกเหตุผล
0
10
-5

สำหรับ B.1 ให้กำหนดช่วงฝึก doubled เป็น -100 ถึง 100 เพื่อให้ผลสองเท่าอยู่ใน int แน่นอน ทดสอบค่าลบ ศูนย์ และค่าบวก โดยยังต้องแสดงว่าค่าเดิมของผู้เรียกอยู่ที่ใดและค่าคืนถูกเก็บที่ใด

งาน sum_to ท้ายบทให้ใช้ n ช่วง 1–100 ในการทดลองและระบุเป็นสัญญา ผลสูงสุด 5050 อยู่ในช่วงขั้นต่ำของ int ไม่ให้ค่าผิดช่วงผ่านไปเข้าลูป และไม่ขยายช่วงโดยยังใช้ชนิดเดิมอย่างไร้ขอบเขต

เก็บกวาด

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

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

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

  1. ต้นแบบกับนิยามทำหน้าที่ต่างกันอย่างไร และคำสั่งเรียกอยู่ที่ไหน
  2. เมื่อ add_bonus(score) คืนค่าแต่ไม่ได้เก็บ score ของผู้เรียกเปลี่ยนหรือไม่
  3. การกำหนด score = add_bonus(score) เปลี่ยนค่าด้วยคำสั่งใดในผู้เรียก
  4. เหตุใดจำนวนชุด 0 กับค่าผิดพลาด -1 ต้องแสดงความหมายต่างกัน
  5. เมื่อออกจากบล็อกย่อย ชื่อที่เคยบังพารามิเตอร์ยังถูกอ้างถึงหรือไม่
  6. เหตุใดฟังก์ชันที่พิมพ์ค่าอย่างเดียวจึงใช้ต่อแตกต่างจากฟังก์ชันคืนค่า

เมื่ออธิบายได้ ให้เตรียมคิดว่าถ้ามีข้อมูลหลายค่าจะจัดเก็บและส่งเข้าไปคำนวณอย่างไร แล้วต่อที่ บทที่ 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 แล้วพบว่าค่าเดิมไม่เปลี่ยน จงวิเคราะห์สาเหตุและเสนอวิธีแก้ให้โปรแกรมนำผลที่ฟังก์ชันคำนวณกลับไปใช้ได้

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

  1. ศึกษาเรื่องฟังก์ชัน การประกาศต้นแบบ และการส่งอาร์กิวเมนต์เพิ่มเติมจาก Kernighan และ Ritchie (1988) บทที่ 4 ว่าด้วยฟังก์ชันและโครงสร้างโปรแกรม แล้วทดลองแยกโปรแกรมที่เขียนไว้ในบทก่อนออกเป็นฟังก์ชันย่อย
  2. ทบทวนความหมายของการส่งค่าแบบ by value และขอบเขตตัวแปรในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจกฎการมองเห็นและอายุของตัวแปรภายในฟังก์ชัน
  3. ทดลองออกแบบโปรแกรมหนึ่งเรื่องโดยแบ่งเป็นฟังก์ชันที่มีหน้าที่เดียวหลายตัว แล้วเปรียบเทียบความอ่านง่ายและความสะดวกในการทดสอบกับการเขียนทั้งหมดไว้ใน 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

ตรวจเอกสารก่อนเรียบเรียงเมื่อ 23 ตุลาคม 2569 ตัวอย่างใช้ hosted C11 และ int main(void) ไม่ใช้รูปแบบ GNU เฉพาะบางตัวอย่าง กรณีทดลองคอมไพล์ผิดใช้เพื่ออ่านคำวินิจฉัย ไม่รันโค้ดที่ยังผิด

By Watcharin Sarachai