Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 2 ตัวแปร ชนิดข้อมูล ค่าคงที่ และนิพจน์
แผนบริหารการสอนประจำบทที่ 2
บทนี้ต่อยอดจากบทที่ 1 ที่ได้ฝึกเขียน คอมไพล์ และรันโปรแกรมภาษา C โปรแกรมแรก โดยเจาะลึกหน่วยพื้นฐานที่ใช้สร้างโปรแกรมทุกตัว คือ ตัวแปร ชนิดข้อมูล ค่าคงที่ และนิพจน์ มุ่งให้ประกาศตัวแปรและเลือกชนิดข้อมูลได้เหมาะสม เขียนนิพจน์ด้วยตัวดำเนินการเลขคณิตอย่างถูกต้อง เข้าใจผลของการหารจำนวนเต็ม และแปลงชนิดข้อมูลเพื่อให้ผลลัพธ์ถูกต้อง การแสดงผลในบทนี้ใช้ printf ที่รู้จักแล้วจากบทที่ 1 ส่วนการรับข้อมูลเข้าด้วย scanf เป็นเนื้อหาของบทที่ 3
| ผลลัพธ์ | ข้อความ | หลักฐาน | CLO |
|---|---|---|---|
| LLO2.1 | อธิบายและเลือกใช้ชนิดข้อมูลพื้นฐาน (int, float, double, char) และประกาศตัวแปร/ค่าคงที่ได้เหมาะสม | งานข้อ 1 (ปฏิบัติการที่ 2 Lab A) | CLO1 |
| LLO2.2 | เขียนนิพจน์ด้วยตัวดำเนินการเลขคณิต (+ - * / %) และอธิบายลำดับความสำคัญและการหารจำนวนเต็ม | งานข้อ 2 (ปฏิบัติการที่ 2 Lab B) | CLO1 |
| LLO2.3 | เขียนโปรแกรมคำนวณและแสดงผลค่าตัวแปรได้ถูกต้องตามไวยากรณ์ รวมถึงการแปลงชนิดข้อมูล (type cast) | งานข้อ 3 (ปฏิบัติการที่ 2 Lab B) | CLO3 |
| LLO2.4 | ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้ | โค้ดทุกข้อที่ส่งในบทนี้ | CLO5 |
ผลลัพธ์การเรียนรู้ของบทนี้ผู้สอนยืนยันแล้ว (มติผู้สอน 21 ต.ค. 2569) และเพิ่ม LLO2.4 มาตรฐานการเขียนโค้ดตามมติผู้สอน 28 ต.ค. 2569 (ดูบันทึกสำหรับผู้สอนท้ายบท)
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO1 อธิบายและใช้ตัวแปร ชนิดข้อมูล การรับ-แสดงผล และโครงสร้างควบคุม (if/else, switch, loop) เพื่อแก้ปัญหาและได้ผลลัพธ์ที่ถูกต้อง
- CLO3 สามารถเขียนโค้ดได้ถูกต้องตามหลักไวยากรณ์ (Syntax) และมีประสิทธิภาพ (Efficiency)
บทที่ 2 เป็นจุดตรวจต่อเนื่องของ CLO1 และ CLO3 ต่อจากบทที่ 1 งานทั้งสามข้อในบทนี้สะสมหลักฐานด้านการเลือกชนิดข้อมูลและการเขียนนิพจน์ที่ถูกต้อง ซึ่งเป็นพื้นฐานที่ส่งต่อไปยังการรับ-แสดงผลในบทที่ 3 และโครงสร้างควบคุมในบทที่ 4–5 ใบงานบทนี้คิด 10 คะแนนตามตารางผลลัพธ์ด้านล่าง และเป็นหนึ่งใน 13 ชิ้นที่มีน้ำหนักเท่ากันในหมวดใบงานบทที่ 1–13 รวม 15% ของคะแนนรายวิชา ตามแผนคะแนนฉบับที่ 4
วิธีสอน
- ทบทวนโปรแกรมภาษา C โปรแกรมแรกจากบทที่ 1 แล้วนำเข้าสู่คำถามว่าจะเก็บและคำนวณค่าต่าง ๆ ในโปรแกรมได้อย่างไร
- อธิบายแนวคิดตัวแปรและการประกาศด้วยรูปแบบ
ชนิด ชื่อ = ค่า;พร้อมเชื่อมโยงกับมาตรฐานการตั้งชื่อแบบ snake_case จากบทที่ 1 - เปรียบเทียบชนิดข้อมูลพื้นฐานและค่าคงที่ด้วยตารางและตัวอย่างสั้น ๆ โดยชี้ให้เห็นการเลือกชนิดให้เหมาะกับข้อมูล
- สาธิตการเขียนนิพจน์และการรันโปรแกรม
ch2_demo.cบนเครื่องจริง เน้นผลของการหารจำนวนเต็มและการแปลงชนิดข้อมูล - ให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
- เอกสารประกอบบทที่ 2 และสไลด์สรุปชนิดข้อมูลและลำดับความสำคัญของตัวดำเนินการ
- ไฟล์โปรแกรมตัวอย่าง
ch2_demo.cที่ทดสอบแล้ว
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 3 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 3 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | ประกาศตัวแปรชนิด int, double, char และค่าคงที่ const แล้วแสดงค่าด้วย printf | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO2.1 | CLO1 |
| 2 | ทำนายและตรวจผลของนิพจน์ ได้แก่ การหารจำนวนเต็ม เศษเหลือ และลำดับความสำคัญ | ตารางทำนาย + ผลการรัน | LLO2.2 | CLO1 |
| 3 | เขียนโปรแกรมคำนวณที่มีการแปลงชนิดข้อมูล (double cast) ให้คอมไพล์ผ่านและผลถูกต้อง | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO2.3 | CLO3 |
เงื่อนไขผ่าน: งานข้อ 3 ต้องคอมไพล์ผ่านโดยไม่มีข้อผิดพลาดก่อน จึงจะประเมินความถูกต้องของผลลัพธ์และมาตรฐานโค้ดได้
คะแนนใบงาน (10 คะแนน)
| ผลลัพธ์ | สิ่งที่ตรวจ | คะแนน |
|---|---|---|
| LLO2.1 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO2.2 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO2.3 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO2.4 | มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง | 1 |
ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา
เกณฑ์ให้คะแนน
งานข้อ 2 และงานข้อ 3 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:
| เกณฑ์ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| งานข้อ 2 | ทำนายผลนิพจน์ถูกต้องและผลการรันตรงกับที่ทำนาย | ทำนายผิดหรือผลการรันไม่ตรง |
| งานข้อ 3 | คอมไพล์ผ่านไม่มีข้อผิดพลาด และผลลัพธ์เป็นทศนิยมที่ถูกต้อง | คอมไพล์ไม่ผ่านหรือผลถูกตัดเศษ |
งานข้อ 1 ใช้รายการตรวจ (checklist): เลือกชนิดข้อมูลเหมาะกับค่าที่กำหนด, ประกาศตัวแปรและค่าคงที่ถูกไวยากรณ์, แสดงค่าครบทุกตัวด้วย printf, ตั้งชื่อสื่อความหมายแบบ snake_case
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
บทนำ
ทุกโปรแกรมต้องเก็บข้อมูลไว้ประมวลผล ไม่ว่าจะเป็นจำนวนเต็ม จำนวนที่มีทศนิยม หรืออักขระ ที่เก็บข้อมูลเหล่านี้เรียกว่า ตัวแปร (variable) และข้อมูลแต่ละอย่างมี ชนิดข้อมูล (data type) ที่กำหนดว่าจะเก็บค่าแบบใดและทำอะไรกับค่านั้นได้ บทนี้อธิบายการประกาศตัวแปร การเลือกชนิดข้อมูลพื้นฐาน การกำหนดค่าคงที่ และการเขียนนิพจน์ด้วยตัวดำเนินการเลขคณิต ซึ่งเป็นหน่วยพื้นฐานที่ใช้สร้างโปรแกรมที่ซับซ้อนขึ้นในบทถัดไป
เนื้อหาบทนี้ต่อยอดโดยตรงจากบทที่ 1 คือใช้โครงร่างโปรแกรมภาษา C ขั้นต่ำ คำสั่งคอมไพล์ และการแสดงผลด้วย printf ที่รู้จักแล้ว ส่วนชนิดข้อมูล ค่าคงที่ ตัวดำเนินการ และการแปลงชนิดข้อมูลที่เริ่มในบทนี้จะถูกใช้ซ้ำในการรับ-แสดงผลบทที่ 3 โครงสร้างควบคุมบทที่ 4–5 และตลอดรายวิชา
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
| ชนิดข้อมูลพื้นฐาน (int, float, double, char) และการประกาศตัวแปร | บท 3 เป็นต้นไป (ทุกปฏิบัติการ) |
| ค่าคงที่ด้วย const และตัวดำเนินการเลขคณิตพร้อมลำดับความสำคัญ | บท 4–5 (เงื่อนไขและการวนซ้ำ) |
| การแปลงชนิดข้อมูล (type cast) | บท 3 (การจัดรูปแบบการแสดงผล) เป็นต้นไป |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE เช่นเดียวกับบทที่ 1 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น เช่น MinGW บนวินโดวส์ คำสั่งคอมไพล์และชื่อไฟล์ปฏิบัติการอาจแตกต่างเล็กน้อย แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน
วิธีใช้บทเรียนและใบงานในหน้านี้
บทนี้นำโจทย์จัดชุดอุปกรณ์จากบทที่ 1 มาเขียนเป็นตัวแปรและนิพจน์ ใช้ค่าคงที่ในซอร์สเพื่อฝึกคิดชนิดและผลการคำนวณก่อน การรับข้อมูลด้วย scanf จะเรียนในบทที่ 3 จึงไม่ต้องเพิ่มการรับข้อมูล ลูป หรือเงื่อนไขเพื่อทำใบงานบทนี้
คัดตารางสำหรับบันทึกลงสมุดหรือไฟล์ของตนเอง เขียนคำทำนายก่อนรัน แล้วจดผลจริงโดยไม่แก้คำทำนายย้อนหลัง ตารางในบทความเป็นแบบฝึกสำหรับจด ไม่ใช่ช่องกรอกที่เว็บไซต์บันทึกคำตอบให้ เมื่อผลต่างกันให้อธิบายชนิดของตัวถูกดำเนินการ ลำดับการจัดกลุ่ม และจุดที่ค่าไปเก็บในตัวแปร
ใช้แพลตฟอร์ม C ของรายวิชาและข้อมูลสมมติ ไม่ต้องติดตั้งเครื่องมือหรือเปิดบัญชีเพิ่ม งานส่งและเกณฑ์ประเมินยังใช้ตามประกาศเดิม
2.1 ตัวแปรและการประกาศ
ระดับที่ 1: ตัวแปร (variable) คือที่เก็บค่าในหน่วยความจำซึ่งมีชื่อกำกับและเปลี่ยนค่าได้ระหว่างที่โปรแกรมทำงาน การนำตัวแปรมาใช้ต้อง ประกาศ (declaration) ก่อน เพื่อบอกคอมไพเลอร์ว่าตัวแปรนั้นชื่ออะไรและเก็บข้อมูลชนิดใด
ระดับที่ 2: รูปแบบการประกาศตัวแปรพร้อมกำหนดค่าเริ่มต้นคือ ชนิด ชื่อ = ค่า; เช่น int radius = 5; ประกาศตัวแปรจำนวนเต็มชื่อ radius และกำหนดค่าเริ่มต้นเป็น 5 การตั้งชื่อตัวแปรใช้มาตรฐานเดียวกับบทที่ 1 คือชื่อสื่อความหมายแบบ snake_case ขึ้นต้นด้วยตัวอักษรหรือขีดล่าง ตามด้วยตัวอักษร ตัวเลข หรือขีดล่าง และต้องไม่ซ้ำกับคำสงวนของภาษา เช่น int หรือ return
ตารางต่อไปนี้เปรียบเทียบชื่อตัวแปรที่ใช้ได้กับใช้ไม่ได้ เพื่อให้เห็นกฎการตั้งชื่อชัดเจน
| ชื่อที่เขียน | ใช้ได้หรือไม่ | เหตุผล |
|---|---|---|
radius | ใช้ได้ | ขึ้นต้นด้วยตัวอักษร สื่อความหมาย |
student_id | ใช้ได้ | รูปแบบ snake_case ขึ้นต้นด้วยตัวอักษร |
_count | ใช้ได้ | ขึ้นต้นด้วยขีดล่างได้ |
avg2 | ใช้ได้ | มีตัวเลขได้หากไม่ได้อยู่ตำแหน่งแรก |
2nd_score | ใช้ไม่ได้ | ขึ้นต้นด้วยตัวเลข |
avg score | ใช้ไม่ได้ | มีช่องว่างคั่นกลาง |
int | ใช้ไม่ได้ | ตรงกับคำสงวนของภาษา |
ระดับที่ 3: การเลือกชนิดข้อมูลให้เหมาะกับค่าที่จะเก็บมีผลต่อคุณภาพและความถูกต้องของโปรแกรม เช่น การเก็บส่วนสูง 1.75 เมตรด้วยชนิดจำนวนเต็มจะทำให้ค่าถูกตัดเศษเหลือ 1 ซึ่งผิดจากความเป็นจริง การตั้งชื่อที่สื่อความหมายยังช่วยให้อ่านและบำรุงรักษาโค้ดได้ง่าย สอดคล้องกับ CLO5 ที่เน้นตั้งแต่บทที่ 1 ข้อบกพร่องที่พบบ่อยของผู้เริ่มต้นคือใช้ตัวแปรโดยไม่ประกาศ หรือประกาศผิดชนิดจนค่าที่เก็บคลาดเคลื่อน
คำถามตรวจความเข้าใจ 2.1: เหตุใดการเก็บส่วนสูง 1.75 จึงควรใช้ชนิด double แทน int
2.2 ชนิดข้อมูลพื้นฐานและค่าคงที่
ระดับที่ 1: ภาษา C มีชนิดข้อมูลพื้นฐานที่ใช้บ่อยสี่ชนิด ได้แก่ int สำหรับจำนวนเต็ม float และ double สำหรับจำนวนที่มีทศนิยม และ char สำหรับอักขระหนึ่งตัว
ระดับที่ 2: ชนิดข้อมูลพื้นฐาน ขนาด ช่วงค่าโดยประมาณ และการใช้งานสรุปได้ดังตาราง ขนาดและช่วงค่าในตารางเป็นค่าตัวอย่างบนระบบที่ทดสอบ (64 บิต Linux/gcc) เพราะมาตรฐานภาษา C รับประกันเพียงช่วงต่ำสุดเท่านั้น (ดูหมายเหตุใต้ตาราง)
| ชนิด | ขนาด (ตัวอย่างบนระบบที่ทดสอบ) | ช่วงค่าโดยประมาณ (ตัวอย่างบนระบบที่ทดสอบ) | ความละเอียด | ตัวอย่างค่า |
|---|---|---|---|---|
int | 4 ไบต์ | ประมาณ -2,147,483,648 ถึง 2,147,483,647 | จำนวนเต็ม | 5, -12, 0 |
float | 4 ไบต์ | ประมาณ ±3.4 × 10^38 | ประมาณ 6–7 หลักนัยสำคัญ | 3.14f |
double | 8 ไบต์ | ประมาณ ±1.8 × 10^308 | ประมาณ 15–16 หลักนัยสำคัญ | 3.14159 |
char | 1 ไบต์ | ประมาณ -128 ถึง 127 | อักขระหนึ่งตัว (รหัส ASCII) | 'A', 'z' |
มาตรฐานภาษา C รับประกันเพียง ช่วงค่าต่ำสุด ไม่ได้บังคับขนาดไบต์ตายตัว เช่น int ต้องกว้างอย่างน้อย 16 บิต (เก็บได้อย่างน้อย -32767 ถึง 32767) และต้องมีลำดับ char ไม่เกิน short ไม่เกิน int ไม่เกิน long ขนาดจริงจึงขึ้นกับระบบและคอมไพเลอร์ ค่าในตารางด้านบนเป็นค่าที่พบบนระบบ 64 บิต (Linux/gcc) ที่ใช้ทดสอบบทนี้ (อ้างอิง cppreference — Arithmetic types)
ใบงาน 1: เลือกชนิดจากความหมาย หน่วย และขอบเขต
สมมติคุณจัดเตรียมอุปกรณ์หนึ่งรายการ ก่อนประกาศตัวแปรให้บอกว่าแต่ละค่ามีไว้คำนวณหรือระบุรายการ ต้องรักษาส่วนเศษหรือไม่ และค่าจะเปลี่ยนระหว่างทำงานหรือควรคงเดิม ชนิดที่รับค่าหนึ่งได้ไม่ได้แปลว่าเหมาะกับความหมายของข้อมูลทุกแบบ
| ข้อมูลสมมติ | หน่วยหรือหน้าที่ | ชนิดที่คุณเลือก | เปลี่ยนค่าได้หรือควรใช้ const | เหตุผล |
|---|---|---|---|---|
| จำนวนสาย 17 | เส้น | บันทึกเอง | บันทึกเอง | พิจารณาว่านับเป็นส่วนเศษได้หรือไม่ |
| จำนวนสายต่อชุด 3 | เส้นต่อชุด | บันทึกเอง | บันทึกเอง | กำหนดว่าจะใช้ขนาดชุดเดิมตลอดตัวอย่างหรือไม่ |
| ราคา 1250 | สตางค์ต่อชิ้น | บันทึกเอง | บันทึกเอง | แยกหน่วยออกจากบาท |
| น้ำหนักเฉลี่ย 1.75 | กิโลกรัม | บันทึกเอง | บันทึกเอง | ต้องรักษาส่วนเศษ |
กลุ่มตัวอย่าง 'B' | ป้ายอักขระหนึ่งตัว | บันทึกเอง | บันทึกเอง | ไม่ใช่ข้อความหลายตัว |
รหัส "0017" | ตัวระบุรายการ | อธิบายแนวคิด | บันทึกเหตุผล | ต้องรักษาศูนย์นำหน้า จึงไม่ใช่ปริมาณที่นำไปบวก |
สำหรับรหัสที่ต้องรักษารูปข้อความ ให้บันทึกเหตุผลไว้ก่อน การเก็บสตริงจะเรียนในบทที่ 8 ไม่ต้องเขียนโค้ดสตริงเพื่อให้ผ่านงานนี้ ส่วนตัวอย่างรหัสจำนวนเต็มใน Lab A ใช้ฝึกชนิดข้อมูลบนสภาพแวดล้อมที่ระบุ ไม่ได้เป็นข้อสรุปว่ารหัสทุกชนิดควรเก็บเป็น int
เมื่อเทียบกับตารางขนาดชนิดข้อมูล ให้แยก “ขนาดที่เครื่องนี้รายงาน” ออกจาก “สิ่งที่มาตรฐานรับประกัน” ใช้ผล sizeof ที่รันได้จริงของตน ไม่คัดตัวเลขจากตัวอย่างมาอ้างว่าเป็นผลทดลองของเครื่องตน
เพื่อยืนยันขนาดจริงบนระบบที่ใช้สอน สามารถใช้ตัวดำเนินการ sizeof ซึ่งคืนขนาดของชนิดข้อมูลเป็นจำนวนไบต์ และแสดงด้วยตัวระบุรูปแบบ %zu
/* types_demo.c : แสดงขนาด (sizeof) ของชนิดข้อมูลพื้นฐาน */
#include <stdio.h>
int main(void) {
printf("sizeof(char) = %zu byte\n", sizeof(char));
printf("sizeof(int) = %zu byte\n", sizeof(int));
printf("sizeof(float) = %zu byte\n", sizeof(float));
printf("sizeof(double) = %zu byte\n", sizeof(double));
return 0;
}ผลการรันจริง (ตัวอย่างบนระบบที่ทดสอบด้วย gcc 13.3.0)
sizeof(char) = 1 byte
sizeof(int) = 4 byte
sizeof(float) = 4 byte
sizeof(double) = 8 byteชนิด char เก็บค่าจำนวนเต็มขนาดเล็กที่ใช้แทนไบต์ของข้อความ ระบบที่ใช้ทดสอบในบทนี้เข้ารหัสอักขระพื้นฐานสอดคล้องกับ ASCII แต่ C11 ไม่บังคับให้ทุกระบบใช้ ASCII เช่น อักขระ 'A' มีรหัส 65 (ทดสอบแล้วด้วย gcc 13.3.0 ดูผลการรันในปฏิบัติการที่ 2 Lab B) ค่าที่ไม่ควรเปลี่ยนระหว่างโปรแกรมทำงาน เช่น ค่าพาย ควรประกาศเป็น ค่าคงที่ (constant) ด้วยคำสำคัญ const เช่น const double PI = 3.14159; เมื่อประกาศเป็น const แล้ว หากมีคำสั่งพยายามเปลี่ยนค่าในภายหลัง คอมไพเลอร์จะแจ้งข้อผิดพลาด อีกวิธีหนึ่งที่พบในโค้ดภาษา C คือการนิยามด้วย #define ที่ต้นไฟล์ เช่น #define PI 3.14159 ซึ่งเป็นการแทนที่ข้อความก่อนแปล ตารางต่อไปนี้เปรียบเทียบสองวิธีนี้
| ประเด็น | const double PI = 3.14159; | #define PI 3.14159 |
|---|---|---|
| เกิดขึ้นเมื่อ | ตอนคอมไพล์ เป็นตัวแปรที่เปลี่ยนค่าไม่ได้ | ก่อนแปล (preprocessor) แทนที่ข้อความ PI ด้วย 3.14159 |
| มีชนิดข้อมูล | มี (double) คอมไพเลอร์ตรวจชนิดให้ | ไม่มีชนิดในตัวเอง เป็นการแทนข้อความล้วน |
ปิดท้ายด้วย ; | ต้องมี ; | ไม่ใส่ ; (ไม่เช่นนั้น ; จะถูกแทนเข้าไปด้วย) |
| ป้องกันการแก้ค่า | ได้ หากแก้ค่าภายหลังคอมไพเลอร์แจ้งข้อผิดพลาด | ได้โดยอ้อม เพราะเป็นเพียงข้อความคงที่ |
ระดับที่ 3: float และ double ต่างกันที่ความละเอียดในการเก็บทศนิยม โดย double เก็บได้ละเอียดกว่า (ประมาณ 15–16 หลักนัยสำคัญ เทียบกับ float ประมาณ 6–7 หลัก) และเป็นชนิดที่นิยมใช้เป็นค่าตั้งต้นสำหรับงานคำนวณทั่วไป ส่วน float ใช้เมื่อต้องการประหยัดหน่วยความจำและยอมรับความละเอียดที่น้อยลงได้ การเลือกชนิดให้เหมาะกับข้อมูลเป็นส่วนหนึ่งของคุณภาพโค้ด ข้อบกพร่องที่พบบ่อยคือใช้ int เก็บค่าที่มีเศษทศนิยม หรือลืมประกาศค่าที่ไม่ควรเปลี่ยนให้เป็น const จนถูกแก้ค่าโดยไม่ตั้งใจ
คำถามตรวจความเข้าใจ 2.2: การประกาศค่าพายเป็น const double PI = 3.14159; ให้ประโยชน์ด้านคุณภาพโค้ดอย่างไรเมื่อเทียบกับการใช้ตัวแปรธรรมดา
2.3 ตัวดำเนินการและนิพจน์
ระดับที่ 1: นิพจน์ (expression) คือการนำค่าหรือตัวแปรมาเชื่อมด้วยตัวดำเนินการเพื่อให้ได้ผลลัพธ์ ตัวดำเนินการเลขคณิตพื้นฐานในภาษา C ได้แก่ + (บวก) - (ลบ) * (คูณ) / (หาร) และ % (หาเศษเหลือ)
ระดับที่ 2: ตารางต่อไปนี้สรุปตัวดำเนินการเลขคณิตพร้อมตัวอย่างและผลลัพธ์ (ทุกค่าทดสอบแล้วด้วย gcc 13.3.0)
| ตัวดำเนินการ | ความหมาย | ตัวอย่าง | ผลลัพธ์ |
|---|---|---|---|
+ | บวก | 7 + 2 | 9 |
- | ลบ | 7 - 2 | 5 |
* | คูณ | 7 * 2 | 14 |
/ | หาร | 7 / 2 | 3 (หารจำนวนเต็ม) |
% | เศษเหลือ | 7 % 2 | 1 |
เมื่อตัวถูกดำเนินการทั้งสองเป็นจำนวนเต็ม ตัวดำเนินการ / จะเป็นการหารจำนวนเต็มที่ตัดเศษทิ้ง เช่น 7 / 2 ได้ผลเป็น 3 ไม่ใช่ 3.5 ส่วน % ใช้หาเศษเหลือของการหารจำนวนเต็ม เช่น 7 % 2 ได้ 1 และ % ใช้ได้กับจำนวนเต็มเท่านั้น
เมื่อนิพจน์มีตัวดำเนินการหลายตัว ลำดับความสำคัญ (operator precedence) กำหนดว่าตัวใดทำงานก่อน ตารางด้านล่างเรียงจากทำงานก่อน (ผูกแน่นที่สุด) ไปทำงานทีหลัง (อ้างอิง cppreference — C operator precedence)
| ลำดับ | ตัวดำเนินการ | ทิศทางการจับคู่ |
|---|---|---|
| สูงสุด | (นิพจน์) วงเล็บ | จากในวงเล็บออกมา |
| รองลงมา | + - เอกภาค และการ cast (ชนิด) | ขวาไปซ้าย |
| กลาง | * / % | ซ้ายไปขวา |
| ต่ำสุด | + - ทวิภาค | ซ้ายไปขวา |
สรุปคือ *, / และ % ทำงานก่อน + และ - และใช้วงเล็บเพื่อเปลี่ยนลำดับได้ เช่น 2 + 3 * 4 ได้ 14 แต่ (2 + 3) * 4 ได้ 20
ใบงาน 2: แยกการจัดกลุ่มก่อนคำนวณผล
ก่อนอ่านตัวอย่างการคำนวณด้านล่าง ให้เติมวงเล็บเพื่อแสดงการจัดกลุ่มของนิพจน์ แล้วลดทีละส่วนจนเหลือคำตอบ ไม่ใช้คำตอบจากเครื่องคิดเลขแทนคำอธิบาย เพราะเครื่องคิดเลขอาจใช้การหารคนละแบบกับ C
| นิพจน์ | วงเล็บที่แสดงการจัดกลุ่ม | ขั้นย่อยแรก | ผลที่คาด | ผลจาก C |
|---|---|---|---|---|
2 + 3 * 4 | บันทึกเอง | บันทึกเอง | บันทึกก่อนรัน | บันทึกหลังรัน |
(2 + 3) * 4 | บันทึกเอง | บันทึกเอง | บันทึกก่อนรัน | บันทึกหลังรัน |
20 / 3 * 3 | บันทึกเอง | บันทึกเอง | บันทึกก่อนรัน | บันทึกหลังรัน |
20 / (3 * 3) | บันทึกเอง | บันทึกเอง | บันทึกก่อนรัน | บันทึกหลังรัน |
2 + 10 % 3 * 4 | บันทึกเอง | บันทึกเอง | บันทึกก่อนรัน | บันทึกหลังรัน |
ตารางลำดับความสำคัญใช้บอกการจัดกลุ่ม ไม่ควรนำไปสรุปลำดับการประเมินอาร์กิวเมนต์หรือผลข้างเคียงทุกแบบ ในบทนี้ใช้นิพจน์ค่าขนาดเล็กที่ไม่มีการแก้ค่าระหว่างนิพจน์ และแยกคำสั่งกำหนดค่าทีละบรรทัดเพื่อให้ติดตามได้ชัด
เมื่อทดสอบ 20 / 3 * 3 ให้ถามว่ามีส่วนเศษถูกตัดในขั้นใด และเหตุใดการคูณกลับจึงไม่กู้เศษเดิม การอธิบายขั้นที่ข้อมูลเปลี่ยนสำคัญกว่าการจำว่าคำตอบสุดท้ายต่างกัน
ตัวอย่างคำนวณทีละขั้น (worked example) ประเมินนิพจน์ 2 + 10 % 3 * 4 ตามลำดับความสำคัญ โดย % และ * อยู่ลำดับเดียวกันจึงทำจากซ้ายไปขวา
| ขั้น | นิพจน์ที่เหลือ | ตัวดำเนินการที่ทำ | ผลย่อย |
|---|---|---|---|
| 1 | 2 + 10 % 3 * 4 | 10 % 3 | 2 + 1 * 4 |
| 2 | 2 + 1 * 4 | 1 * 4 | 2 + 4 |
| 3 | 2 + 4 | 2 + 4 | 6 |
ตัวอย่างนี้ตรวจได้ด้วยโปรแกรมต่อไปนี้ ซึ่งรวมนิพจน์ที่มีหลายตัวดำเนินการไว้ด้วยกัน
/* precedence_demo.c : ลำดับความสำคัญของตัวดำเนินการเลขคณิต */
#include <stdio.h>
int main(void) {
printf("2 + 3 * 4 = %d\n", 2 + 3 * 4);
printf("(2 + 3) * 4 = %d\n", (2 + 3) * 4);
printf("10 + 20 / 5 - 3 = %d\n", 10 + 20 / 5 - 3);
printf("2 + 10 %% 3 * 4 = %d\n", 2 + 10 % 3 * 4);
return 0;
}ผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
2 + 3 * 4 = 14
(2 + 3) * 4 = 20
10 + 20 / 5 - 3 = 11
2 + 10 % 3 * 4 = 6บรรทัด 10 + 20 / 5 - 3 ได้ 11 เพราะ 20 / 5 ทำก่อน (ได้ 4) เหลือ 10 + 4 - 3 แล้วทำ + และ - จากซ้ายไปขวา ได้ 14 - 3 เท่ากับ 11
ระดับที่ 3: ข้อบกพร่องที่พบบ่อยที่สุดในบทนี้คือการหารจำนวนเต็มโดยไม่ตั้งใจ เช่น ต้องการค่าเฉลี่ยของ 7 กับ 2 แต่เขียน 7 / 2 ได้ 3 เพราะตัวถูกดำเนินการทั้งสองเป็นจำนวนเต็ม ทำให้ผลลัพธ์ถูกตัดเศษก่อนนำไปใช้ ตารางด้านล่างเปรียบเทียบนิพจน์หารที่ชนิดตัวถูกดำเนินการต่างกัน
| นิพจน์ | ชนิดตัวถูกดำเนินการ | ผลลัพธ์ | หมายเหตุ |
|---|---|---|---|
7 / 2 | จำนวนเต็ม หาร จำนวนเต็ม | 3 | หารจำนวนเต็ม ตัดเศษทิ้ง |
7 % 2 | จำนวนเต็ม หาร จำนวนเต็ม | 1 | เศษเหลือของการหาร |
7.0 / 2 | ทศนิยม หาร จำนวนเต็ม | 3.5 | ตัวเลขทศนิยมทำให้เป็นการหารทศนิยม |
(double)7 / 2 | ทศนิยม หาร จำนวนเต็ม | 3.5 | cast ทำให้เป็นการหารทศนิยม |
การเข้าใจลำดับความสำคัญและชนิดของตัวถูกดำเนินการจึงจำเป็นต่อความถูกต้องของผลลัพธ์ ซึ่งนำไปสู่การแปลงชนิดข้อมูลในหัวข้อถัดไป
คำถามตรวจความเข้าใจ 2.3: นิพจน์ 7 / 2 ในภาษา C ให้ค่าใด และเพราะเหตุใดจึงได้ค่านั้น
2.4 การแปลงชนิดข้อมูล
ระดับที่ 1: การแปลงชนิดข้อมูล (type conversion) คือการเปลี่ยนค่าจากชนิดหนึ่งไปเป็นอีกชนิดหนึ่ง มีทั้งแบบที่เกิดขึ้นอัตโนมัติและแบบที่ผู้เขียนโปรแกรมสั่งเองด้วยการ cast
ระดับที่ 2: การสั่งแปลงชนิดเองทำโดยเขียนชนิดเป้าหมายไว้ในวงเล็บหน้าค่า เช่น (double)a แปลงค่าของ a ให้เป็นทศนิยมก่อนคำนวณ เมื่อ a และ b เป็นจำนวนเต็ม การเขียน (double)a / b จะทำให้การหารเป็นการหารทศนิยม เช่น เมื่อ a เป็น 7 และ b เป็น 2 นิพจน์ (double)a / b ให้ผล 3.500 (ทดสอบแล้วด้วย gcc 13.3.0 ดูผลการรันในปฏิบัติการที่ 2 Lab B) ต่างจาก a / b ที่ให้ 3
ใบงาน 3: ชนิดปลายทางไม่ย้อนกลับไปเปลี่ยนการหาร
กำหนด a = 9 และ b = 4 โดยทั้งคู่เป็น int ก่อนอ่านผลทดลอง ให้ระบุชนิดที่ใช้ ณ จุดหาร แล้วจึงพิจารณาชนิดของตัวแปรที่รับผล การเห็น double ด้านซ้ายเพียงอย่างเดียวยังไม่ตอบว่าการหารเป็นแบบใด
| คำสั่งหรือการคำนวณ | ชนิด ณ จุดหาร | ค่าก่อนเก็บปลายทาง | ค่าที่คาดเมื่อแสดงสองตำแหน่ง |
|---|---|---|---|
double result = a / b; | บันทึกเอง | บันทึกเอง | บันทึกเอง |
double result = (double)a / b; | บันทึกเอง | บันทึกเอง | บันทึกเอง |
double result = (double)(a / b); | บันทึกเอง | บันทึกเอง | บันทึกเอง |
double result = a / 4.0; | บันทึกเอง | บันทึกเอง | บันทึกเอง |
ทดลองเปลี่ยนคู่ข้อมูลเป็น 8 กับ 4 และ 1 กับ 4 แล้วอธิบายว่าคู่ใดช่วยพบความผิดเรื่องตำแหน่ง cast ได้ดีกว่า คู่ที่หารลงตัวอาจทำให้หลายวิธีดูเหมือนให้คำตอบเหมือนกัน จึงไม่พอสำหรับยืนยันว่าตำแหน่งการแปลงถูกแล้ว
ใช้ตัวหารที่ไม่เป็นศูนย์และค่าที่แทนด้วยชนิดที่ใช้ได้เท่านั้น กรณีตัวหารศูนย์ให้ระบุว่าอยู่นอกขอบเขตและไม่รัน ไม่ทดลองแล้วนำอาการครั้งหนึ่งไปสรุปเป็นคำตอบของภาษา C
ตัวอย่างคำนวณทีละขั้น (worked example) ตำแหน่งที่วางการ cast มีผลต่อผลลัพธ์ กำหนด a เป็น 9 และ b เป็น 4 แล้วเปรียบเทียบสามนิพจน์ โดยการ cast ผูกแน่นกว่า / แต่ต้องดูวงเล็บด้วยว่าครอบตัวถูกหารหรือผลหาร (ดูตารางลำดับความสำคัญในหัวข้อ 2.3)
| นิพจน์ | ลำดับการคำนวณ | ผลลัพธ์ |
|---|---|---|
a / b | 9 / 4 หารจำนวนเต็ม ตัดเศษ | 2 |
(double)a / b | 9.0 / 4 หารทศนิยม | 2.25 |
(double)(a / b) | 9 / 4 ได้ 2 ก่อน แล้วค่อยแปลงเป็น 2.0 | 2.00 |
จุดสำคัญคือ (double)(a / b) ยังได้ 2.00 เพราะการหารจำนวนเต็มเกิดขึ้นก่อน แล้วจึงแปลงผลที่ตัดเศษแล้วเป็นทศนิยม ส่วน (double)a / b แปลงตัวตั้งก่อนหาร จึงได้ 2.25 ที่ถูกต้อง ตรวจได้ด้วยโปรแกรมต่อไปนี้
/* div_demo.c : การหารจำนวนเต็มเทียบกับการหารทศนิยม */
#include <stdio.h>
int main(void) {
int a = 9, b = 4;
printf("a / b = %d\n", a / b);
printf("a %% b = %d\n", a % b);
printf("(double)a / b = %.4f\n", (double)a / b);
printf("(double)(a / b)= %.4f\n", (double)(a / b));
return 0;
}ผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
a / b = 2
a % b = 1
(double)a / b = 2.2500
(double)(a / b)= 2.0000ระดับที่ 3: การแปลงชนิดมีผลโดยตรงต่อความถูกต้องของผลลัพธ์ โดยเฉพาะเมื่อต้องการค่าเฉลี่ยหรืออัตราส่วนที่เป็นทศนิยมจากข้อมูลจำนวนเต็ม การ cast ตัวถูกดำเนินการอย่างน้อยหนึ่งตัวให้เป็น double ก่อนหาร จะป้องกันการตัดเศษโดยไม่ตั้งใจ และต้องวางการ cast ให้เกิดก่อนการหารดังตัวอย่างข้างต้น ประเด็นการแสดงผลค่าทศนิยมด้วยตัวระบุรูปแบบ เช่น %.2f และ %.3f จะอธิบายอย่างละเอียดในการจัดรูปแบบการแสดงผลบทที่ 3 บทนี้ใช้ตัวระบุรูปแบบ %d สำหรับจำนวนเต็ม %.2f/%.3f สำหรับทศนิยม และ %c สำหรับอักขระ พร้อมหมายเหตุนี้
คำถามตรวจความเข้าใจ 2.4: นิพจน์ (double)a / b ต่างจาก a / b อย่างไรเมื่อ a และ b เป็นจำนวนเต็ม
2.5 กรณีศึกษาต่อเนื่อง: ชุดอุปกรณ์ ราคา และหน่วยข้อมูล
นำโจทย์สายเชื่อมต่อจากบทก่อนมาเขียนเป็นนิพจน์ ข้อมูล 17 เส้นและชุดละ 3 เส้นเป็นจำนวนเต็ม ส่วนราคาเฉลี่ยต่อเส้นอาจมีทศนิยม ก่อนเลือกชนิดข้อมูลให้ถามว่าค่าต้องแทนอะไร มีช่วงเท่าไร และนำไปคำนวณแบบใด ตัวเลขที่หน้าตาคล้ายกันอาจต้องใช้ตัวแทนต่างกัน เช่น จำนวนเส้นเป็นปริมาณ แต่รหัสสินค้าเป็นป้ายระบุ ไม่ควรนำไปบวก
2.5.1 คิดชนิดของนิพจน์ทีละขั้น
#include <stdio.h>
int main(void)
{
int cables = 17;
int per_kit = 3;
int complete_kits = cables / per_kit;
int leftover = cables % per_kit;
double average_a = cables / per_kit;
double average_b = (double)cables / per_kit;
printf("kits=%d leftover=%d\n", complete_kits, leftover);
printf("integer first=%.2f\n", average_a);
printf("convert first=%.2f\n", average_b);
return 0;
}ผลลัพธ์คือ kits=5 leftover=2 ตามด้วย integer first=5.00 และ convert first=5.67 ความแตกต่างไม่ได้เกิดจากจำนวนตำแหน่งทศนิยมที่แสดง แต่เกิดก่อนนั้น ขณะคำนวณ cables / per_kit ตัวถูกหารทั้งสองเป็น int จึงได้ผลหารจำนวนเต็ม 5 แล้วจึงแปลง 5 ไปเก็บเป็น double ส่วนการแปลงตัวตั้งก่อนหารทำให้การหารใช้ชนิดทศนิยม
การเขียน (double)(cables / per_kit) ก็ยังได้ 5.00 เพราะวงเล็บด้านในคำนวณก่อน การแปลงชนิดไม่สามารถกู้ส่วนเศษที่ถูกตัดทิ้งไปแล้วได้ ให้ลากเส้นแบ่งในกระดาษเป็น “ชนิดของตัวตั้ง → ชนิดของการหาร → ค่าที่ได้ → ชนิดของตัวรับ” จะช่วยหยุดความเข้าใจผิดว่าตัวแปรด้านซ้ายบังคับวิธีคำนวณทั้งหมดด้านขวา
| นิพจน์ | สิ่งที่เกิดก่อนเก็บค่า | ค่าทางคณิตศาสตร์ที่เก็บโดยประมาณ |
|---|---|---|
17 / 3 | หารจำนวนเต็ม | 5 |
17.0 / 3 | หารทศนิยม | 5.666… |
(double)17 / 3 | แปลงตัวตั้งก่อนหาร | 5.666… |
(double)(17 / 3) | หารจำนวนเต็มก่อนแปลง | 5.0 |
สำหรับจำนวนเต็มติดลบ การหารใน C11 ตัดส่วนทศนิยมเข้าหาศูนย์ ดังนั้น -17 / 3 ได้ -5 และ -17 % 3 ได้ -2 ไม่ควรนำความจำจากเครื่องคิดเลขหรือภาษาอื่นมาแทนกติกาของ C โจทย์จัดชุดในบทนี้กำหนดให้ปริมาณไม่ติดลบ จึงไม่ต้องใช้ผลลบเป็นข้อมูลใช้งาน แต่ตัวอย่างช่วยให้เห็นว่าขอบเขตข้อมูลมีความสำคัญ GNU C: Integer Arithmetic อธิบายบทบาทของชนิดจำนวนเต็มในการคำนวณ
2.5.2 ใช้จำนวนเต็มแทนหน่วยย่อยเมื่อต้องการยอดแน่นอน
สมมติสมุดราคาชิ้นละ 12 บาท 50 สตางค์ ซื้อ 3 ชิ้น การเก็บราคาเป็น 1250 สตางค์แล้วคูณ 3 ได้ 3750 สตางค์ ทำให้การคำนวณนี้อยู่ในจำนวนเต็มและสื่อหน่วยชัดเจน เมื่อแสดงผลจึงแยกบาทด้วยการหาร 100 และสตางค์ด้วยเศษจาก 100 ตัวอย่างเป็นแบบจำลองราคาง่าย ๆ ไม่รวมภาษี การปัดเศษส่วนลด หรือกติกาธุรกิจอื่นที่ต้องกำหนดเพิ่ม
#include <stdio.h>
int main(void)
{
const int unit_price_satang = 1250;
int quantity = 3;
int total_satang = unit_price_satang * quantity;
printf("Total: %d.%02d baht\n",
total_satang / 100, total_satang % 100);
return 0;
}ผลคือ Total: 37.50 baht ชื่อ total_satang ช่วยเตือนว่าไม่ใช่จำนวนบาท ถ้าเปลี่ยนชื่อเป็น total เฉย ๆ แล้วอีกคนบวกค่าจัดส่ง 20 ที่คิดเป็นบาท ผลจะผิดโดยไม่มีข้อผิดพลาดทางไวยากรณ์ ชนิด int ไม่ได้รู้จักหน่วยสตางค์ จึงต้องใช้ชื่อและข้อกำหนดช่วยป้องกัน
ตัวอย่างนี้ไม่ได้หมายความว่าจำนวนเต็มใช้ได้ทุกยอด ต้องตรวจว่าผลคูณสูงสุดอยู่ในช่วงชนิดด้วย ในบทนี้ใช้ปริมาณเล็กที่กำหนดแน่นอน เมื่อรับค่าจากผู้ใช้ในบทถัดไปต้องตรวจช่วงก่อนคำนวณ การเพิ่มชนิดที่ใหญ่ขึ้นโดยไม่วิเคราะห์ช่วงไม่ได้ทำให้ปัญหา overflow หายไปเสมอ
2.6 ความผิดพลาดที่ดูเหมือนคำนวณถูก
2.6.1 ความแม่นยำในการเก็บกับรูปแบบแสดงเป็นคนละเรื่อง
%.2f กำหนดการแสดงสองตำแหน่ง ไม่ได้เปลี่ยนชนิดของตัวแปรและไม่ได้ทำให้ทุกค่าทศนิยมแทนได้ตรงในฐานสอง ถ้าคำนวณค่าที่ได้ประมาณ 2.345 การแสดงสองตำแหน่งเป็นการจัดรูปแบบผลลัพธ์ของค่าที่เก็บจริง ไม่ควรใช้การพิมพ์ออกมาเหมือนกันเป็นหลักฐานว่าค่าสองค่าภายในเท่ากันทุกบิต
ในงานวัดอุณหภูมิแบบจำลอง การใช้ double เหมาะกับค่าที่มีเศษและการคำนวณทั่วไป แต่ในงานนับอุปกรณ์ไม่ควรยอมให้มีสาย 2.5 เส้นเพียงเพราะ double รับได้ การเลือกชนิดจึงต้องเริ่มจากความหมายของข้อมูล ไม่ใช่เลือกชนิดที่ดูเก็บได้มากที่สุด
2.6.2 ค่าที่เปลี่ยนกับชื่อที่เหมือนเดิม
พิจารณาคำสั่ง count = count + 1; ด้านขวาใช้ค่าเดิมคำนวณก่อน แล้วจึงนำผลไปเก็บทับด้านซ้าย เครื่องหมาย = ในที่นี้ไม่ใช่การประกาศความเท่ากันแบบสมการคณิตศาสตร์ ถ้า count เริ่มที่ 4 หลังคำสั่งเป็น 5 และถ้าทำซ้ำอีกครั้งเป็น 6
ให้เขียน trace สามคอลัมน์ คือคำสั่ง ค่าเดิม และค่าใหม่ สำหรับการปรับยอด total = total + price; แล้วเปรียบเทียบกับ total = price; แบบหลังแทนค่าทั้งหมด ไม่ได้สะสม เหตุผลนี้จะใช้ซ้ำในบทลูป ไม่ควรจำเพียงสัญลักษณ์ += โดยไม่เข้าใจการอ่านค่าเดิม
ใบงาน 4: การกำหนดค่าเป็นเหตุการณ์ ไม่ใช่สายเชื่อมตัวแปร
เมื่อกำหนด snapshot = current; โปรแกรมนำค่าของ current ในขณะนั้นไปเก็บ ไม่ได้สร้างความสัมพันธ์ว่าตัวแปรทั้งสองต้องเปลี่ยนตามกันตลอดเวลา อ่านโปรแกรมต่อไปนี้แล้วเขียนผลแต่ละบรรทัดก่อนรัน
/* assignment_trace.c: Follow assignments one statement at a time. */
#include <stdio.h>
int main(void)
{
int current = 4;
int snapshot = current;
printf("start: current=%d snapshot=%d\n", current, snapshot);
current = current + 1;
printf("increment: current=%d snapshot=%d\n", current, snapshot);
current = 20;
printf("replace: current=%d snapshot=%d\n", current, snapshot);
snapshot = current;
printf("copy again: current=%d snapshot=%d\n", current, snapshot);
return 0;
}| จุดที่สังเกต | current ที่คาด | snapshot ที่คาด | เหตุผล |
|---|---|---|---|
| หลังเริ่มต้น | บันทึกเอง | บันทึกเอง | ตัวแปรใดถูกตั้งค่าจากตัวใด |
หลังเพิ่มค่า current | บันทึกเอง | บันทึกเอง | คำสั่งนี้แก้ตัวแปรใด |
หลังแทน current ด้วย 20 | บันทึกเอง | บันทึกเอง | ค่าก่อนหน้าถูกใช้ต่อหรือถูกแทน |
| หลังคัดลอกค่าอีกครั้ง | บันทึกเอง | บันทึกเอง | จุดใดที่ snapshot เปลี่ยนจริง |
อธิบายความต่างระหว่าง current = current + 1; กับ current = 1; โดยแยกค่าเดิม นิพจน์ด้านขวา และค่าใหม่ที่เก็บด้านซ้าย แนวคิดการเก็บค่าตามคำสั่งอ้างอิงได้จาก GNU C: Assignment Expressions และจะใช้ซ้ำเมื่อเรียนตัวนับกับตัวสะสมในบทลูป
2.7 เวิร์กช็อปขยาย: ตารางชนิด หน่วย และขอบเขต
ออกแบบข้อมูลสำหรับร้านอุปกรณ์สมมติ มีรหัส "0017" จำนวนสินค้า ราคาต่อหน่วยเป็นสตางค์ และน้ำหนักเฉลี่ย รหัสที่ขึ้นต้นด้วยศูนย์ควรรักษารูปข้อความไว้ ซึ่งจะเรียนการเก็บด้วยสตริงในบทที่ 8 ในบทนี้เพียงระบุเหตุผลและแยกรหัสออกจากปริมาณ ไม่ต้องเร่งใช้โครงสร้างที่ยังไม่เรียน
ทำตารางสำหรับแต่ละข้อมูล โดยระบุชื่อ หน่วย ตัวอย่างค่าที่รับได้ ค่าที่ไม่ควรรับ และนิพจน์ที่จะใช้ จากนั้นคำนวณยอดของสมุด 3 ชิ้นกับปากกา 2 ด้ามที่มีราคาในหน่วยเดียวกัน ทดลองใส่จำนวนเป็นศูนย์และตรวจว่าผลยอดศูนย์สอดคล้องกับโจทย์ ไม่ใช่ถือว่าศูนย์ผิดเสมอ
ขั้นถัดไปเปรียบเทียบค่าเฉลี่ยของคะแนนสมมติ 7, 8 และ 10 ด้วยสามนิพจน์ (7 + 8 + 10) / 3, (7 + 8 + 10) / 3.0 และ (double)((7 + 8 + 10) / 3) เขียนคำทำนายก่อนรัน แล้วอธิบายความแตกต่างด้วยชนิดของตัวถูกดำเนินการ ห้ามใช้เพียงคำตอบว่า “ใส่จุดแล้วถูก” เพราะยังไม่บอกว่าเกิดอะไรขึ้น
ท้ายงานให้สร้างตัวอย่างผิดหนึ่งแบบเกี่ยวกับหน่วยและหนึ่งแบบเกี่ยวกับการหาร โดยใช้ค่าที่อยู่ในช่วงปลอดภัย จากนั้นแก้และอธิบายว่าทำไมคอมไพเลอร์อาจยอมรับทั้งฉบับผิดและฉบับถูก หลักฐานการเรียนรู้คือสามารถอธิบายสาเหตุและออกแบบกรณีทดสอบที่แยกสองฉบับออกจากกันได้
2.8 ตัวอย่างเปรียบเทียบ: แปลงหน่วยเวลาโดยไม่สูญเสียเศษ
สมมติงานทดลองใช้เวลา 3671 วินาที ต้องรายงานเป็น 1 ชั่วโมง 1 นาที 11 วินาที เริ่มจากจำนวนชั่วโมงเต็มด้วยการหาร 3600 จากนั้นหาเศษที่ยังไม่ครบชั่วโมง แล้วค่อยแยกเป็นนาทีและวินาที ไม่ใช้ผลหารทศนิยมแล้วเดาตัวเลขหลังจุดว่าเป็นนาที เพราะ 1.5 ชั่วโมงหมายถึงหนึ่งชั่วโมงครึ่ง ไม่ใช่หนึ่งชั่วโมงห้านาที
นิพจน์ที่ใช้คือ hours = total_seconds / 3600, remaining = total_seconds % 3600, minutes = remaining / 60 และ seconds = remaining % 60 หลังแยกแล้วตรวจย้อนกลับว่า hours * 3600 + minutes * 60 + seconds เท่ากับค่าเดิมสำหรับค่าทดสอบที่อยู่ในช่วงปลอดภัย
| วินาทีทั้งหมด | ชั่วโมง | นาที | วินาทีส่วนท้าย |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 59 | 0 | 0 | 59 |
| 60 | 0 | 1 | 0 |
| 3599 | 0 | 59 | 59 |
| 3600 | 1 | 0 | 0 |
| 3671 | 1 | 1 | 11 |
ถ้าเขียน minutes = total_seconds / 60 แล้วรายงานควบกับชั่วโมง จะได้ 61 นาทีสำหรับ 3671 วินาที นั่นเป็นนาทีทั้งหมด ไม่ใช่นาทีที่เหลือหลังแยกชั่วโมง สูตรไม่ได้ผิดโดยตัวมันเอง แต่ไม่ตรงกับความหมายของช่องผลลัพธ์ การตั้งชื่อ total_minutes กับ remaining_minutes ช่วยแยกสองแนวคิด
ทดลองเปลี่ยนหน่วยจากวินาทีเป็นมิลลิวินาทีแล้วระบุว่าค่าคงที่ใดต้องเปลี่ยน ถ้านำค่าหน่วยใหม่เข้าไปโดยยังหาร 3600 จะได้ตัวเลขที่ดูสมเหตุผลแต่หน่วยผิด ตรรกะการแปลงหน่วยนี้เชื่อมกับการเก็บราคาเป็นสตางค์และการใช้ตัวแปรที่มีชื่อบอกหน่วย
ท้ายตัวอย่างให้ผู้เรียนเขียนรายการ “ชนิดข้อมูลรับประกันอะไร” กับ “ชนิดข้อมูลไม่รู้เรื่องอะไร” ชนิดจำนวนเต็มกำหนดรูปแบบการคำนวณและช่วง แต่ไม่รู้ว่าค่าเป็นวินาที บาท หรือชิ้น ความถูกต้องจึงต้องอาศัยทั้งชนิด ชื่อ ขอบเขต และโจทย์ร่วมกัน
ทดลองแปลงหน่วยและตรวจย้อนกลับด้วยโปรแกรม
เมื่อไล่ตารางด้วยมือแล้ว ใช้โปรแกรมต่อไปนี้ตรวจความสัมพันธ์ของหน่วย กำหนดข้อมูลตัวอย่างเป็นจำนวนเต็มไม่ติดลบขนาดเล็กที่ผลคำนวณทั้งหมดอยู่ในช่วง int โปรแกรมไม่มีการรับข้อมูลหรือวนซ้ำ หากต้องการทดลองค่าอื่น ให้แก้ค่าเริ่มต้นแล้วแปลและรันใหม่
/* time_units.c: Decompose a small nonnegative duration into units. */
#include <stdio.h>
int main(void)
{
const int seconds_per_hour = 3600;
const int seconds_per_minute = 60;
int total_seconds = 3671;
int hours = total_seconds / seconds_per_hour;
int remaining_seconds = total_seconds % seconds_per_hour;
int minutes = remaining_seconds / seconds_per_minute;
int seconds_left = remaining_seconds % seconds_per_minute;
int restored = hours * seconds_per_hour
+ minutes * seconds_per_minute + seconds_left;
printf("seconds=%d hours=%d minutes=%d seconds_left=%d\n",
total_seconds, hours, minutes, seconds_left);
printf("restored=%d\n", restored);
return 0;
}ทำนายผลสำหรับ 3671 ก่อนรัน แล้วทดลอง 0, 59, 60, 3599 และ 3600 เพิ่ม โดยบันทึกชั่วโมง นาที วินาทีส่วนท้าย และค่าที่ประกอบกลับ ตรวจว่าค่าที่ประกอบกลับเท่าจำนวนวินาทีเดิม และนาทีกับวินาทีส่วนท้ายอยู่ในช่วง 0–59 สำหรับข้อมูลตามข้อตกลงนี้
การที่โปรแกรมแปลผ่านยังไม่รับรองว่าเลือกหน่วยถูก เช่น การใช้ total_seconds / 60 เป็นนาทีที่เหลือจะสับสนกับนาทีทั้งหมด กรณี 3671 ช่วยเปิดเผยความต่างได้ดีกว่ากรณีที่ยังไม่ถึงหนึ่งชั่วโมง หลักการใช้ผลหารและเศษเพื่อตรวจกลับอ่านเพิ่มเติมได้จาก GNU C: Division and Remainder
2.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: ประกาศชื่อเดิมซ้ำด้วยชนิดที่ขัดกัน
ประเภท: ข้อผิดพลาดขณะคอมไพล์
ตัวแปรชื่อ score ถูกประกาศสองครั้งในขอบเขตเดียวกันด้วยชนิด int และ double การเปลี่ยนชนิดไม่ใช่การประกาศชื่อเดิมทับโดยอัตโนมัติ ต้องเลือกชนิดให้ตรงกับข้อมูลก่อนใช้งาน
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int main(void)
{
int score = 80;
double score = 80.5;
printf("%.1f\n", score);
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: conflicting types for 'score'; have 'double'
5 | double score = 80.5;
| ^~~~~
bad.c:4:9: note: previous definition of 'score' with type 'int'
4 | int score = 80;
| ^~~~~สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
conflicting types ระบุว่าชนิดที่พบขัดกับ declaration เดิม อ่าน note ที่บอก previous definition ควบคู่กับบรรทัด error เพื่อเทียบสองตำแหน่ง ไม่ตรวจเพียงจุดที่ประกาศครั้งหลัง
โค้ดที่แก้แล้ว
#include <stdio.h>
int main(void)
{
double score = 80.5;
printf("%.1f\n", score);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
ใช้ double เพียง declaration เดียวเพื่อเก็บคะแนนที่มีเศษ และคง %.1f สำหรับ printf ซึ่งรับ double การแก้ไม่ได้ใช้การ cast เพื่อกลบการประกาศซ้ำ
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
80.5ตัวอย่างที่ผิด 2: นำผลหารจำนวนเต็มไปเก็บใน double
ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน
ข้อมูลสังเคราะห์กำหนดผลรวม 5 และจำนวน 2 ซึ่งต้องได้ค่าเฉลี่ย 2.50 แต่ operands ทั้งสองของการหารเป็น int จึงหารจำนวนเต็มก่อนแล้วค่อยแปลงผลเป็น double เศษที่สูญเสียไปไม่กลับคืนเพราะตัวแปรปลายทางเป็น double
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int main(void)
{
int total = 5;
int count = 2;
double average = total / count;
printf("average=%.2f\n", average);
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o badไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม
สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ผลจากการรันฉบับผิด
stdout:
average=2.00stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน
สถานะจบที่ตัวเก็บผลอ่านได้: 0
ผลตามข้อกำหนดที่ใช้เทียบ:
average=2.50อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
กรณีนี้ไม่มี compiler error และอาจไม่มี warning ภายใต้ตัวเลือกที่ใช้ log จึงแสดงผลจริงเทียบกับผลคาดหวังแทน ไม่แต่ง error message ให้การคำนวณที่ถูกกติกาภาษาแต่ผิดโจทย์
โค้ดที่แก้แล้ว
#include <stdio.h>
int main(void)
{
int total = 5;
int count = 2;
double average = (double)total / count;
printf("average=%.2f\n", average);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
แปลง total เป็น double ก่อนหารเพื่อให้เกิดการหารทศนิยม แล้วทดสอบด้วยข้อมูลที่หารไม่ลงตัว การ cast ผลหลังหาร เช่น (double)(total / count) ยังช้าเกินไป ต้องตรวจตำแหน่ง conversion
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
average=2.50ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 2 ตัวแปร ชนิดข้อมูล และนิพจน์
ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกประกาศตัวแปรชนิดต่าง ๆ และค่าคงที่ แล้วแสดงค่าด้วย printf ส่วนที่สองฝึกทำนายและตรวจผลของนิพจน์ รวมถึงการแปลงชนิดข้อมูล ทั้งสองส่วนควรทำตามลำดับเพื่อให้เห็นการเชื่อมโยงจากการประกาศตัวแปรสู่การคำนวณ
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0 การแสดงผลใช้ printf ที่รู้จักแล้วจากบทที่ 1 โดยยังไม่ใช้การรับข้อมูลเข้าซึ่งเป็นเนื้อหาของบทที่ 3
Lab A: ประกาศและแสดงค่าตัวแปรข้อมูลนักศึกษา
สถานการณ์ ต้องการเก็บข้อมูลนักศึกษาหนึ่งคน ได้แก่ รหัสนักศึกษาเป็นจำนวนเต็ม เกรดเฉลี่ยเป็นจำนวนทศนิยม และอักษรย่อกลุ่มเรียนเป็นอักขระหนึ่งตัว พร้อมค่าคงที่ของวิชา แล้วแสดงค่าทั้งหมดออกทางจอภาพ
เป้าหมาย ฝึก LLO2.1 คือเลือกชนิดข้อมูลให้เหมาะกับค่า ประกาศตัวแปรและค่าคงที่ และแสดงค่าด้วย printf
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
student_info.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* student_info.c : ประกาศและแสดงค่าตัวแปรข้อมูลนักศึกษาหนึ่งคน */
#include <stdio.h>
int main(void) {
const int CREDIT = 3;
int student_id = 650000001;
double gpa = 3.75;
char section = 'B';
printf("student_id = %d\n", student_id);
printf("gpa = %.2f\n", gpa);
printf("section = %c\n", section);
printf("credit = %d\n", CREDIT);
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall student_info.c -o student_info
./student_infoผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
student_id = 650000001
gpa = 3.75
section = B
credit = 3ในโค้ดข้างต้น %d เป็นตัวระบุรูปแบบสำหรับจำนวนเต็ม %.2f สำหรับทศนิยมสองตำแหน่ง และ %c สำหรับอักขระหนึ่งตัว รายละเอียดของตัวระบุรูปแบบและการจัดรูปแบบการแสดงผลจะอธิบายในบทที่ 3
ตัดสินใจเอง A.1 ออกแบบชุดข้อมูลนักศึกษาสมมติของตนเอง แล้วเลือกชนิดข้อมูลให้เหมาะกับแต่ละค่า พร้อมเขียนเหตุผลประกอบสั้น ๆ ว่าเหตุใดจึงเลือกชนิดนั้น และระบุว่าค่าใดควรเป็นค่าคงที่
ผลลัพธ์ที่คาดหวัง A.1 โปรแกรมคอมไพล์ผ่านโดยไม่มีข้อผิดพลาดและไม่มีการแจ้งเตือน และแสดงค่าทุกตัวครบถ้วนด้วยชนิดข้อมูลที่เหมาะสม
จุดตรวจ A ประกาศตัวแปรครบทุกชนิดที่กำหนด (จำนวนเต็ม ทศนิยม อักขระ) และค่าคงที่ด้วย const แสดงค่าถูกต้องด้วย printf และตั้งชื่อสื่อความหมายแบบ snake_case
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| เกรดเฉลี่ยแสดงเป็นจำนวนเต็ม | ประกาศเกรดเฉลี่ยด้วย int แทน double | เปลี่ยนชนิดเป็น double ให้เก็บค่าทศนิยมได้ |
| ค่าคงที่ของวิชาถูกแก้โดยไม่ตั้งใจ | ลืมประกาศด้วย const | ประกาศค่าที่ไม่ควรเปลี่ยนด้วย const |
| อักขระแสดงเป็นตัวเลข | ใช้ %d กับตัวแปร char ที่ต้องการแสดงเป็นอักขระ | ใช้ %c เพื่อแสดงอักขระ |
เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว
ใบงานตรวจ Lab A: ชนิด ค่าที่เก็บ และผลที่แสดง
ใช้ชุดข้อมูลสมมติสำหรับฝึก ไม่ต้องใส่รหัส เกรด หรือข้อมูลส่วนตัวจริงลงในโปรแกรม แยกให้ได้ว่าค่าใดเป็นปริมาณที่นำไปคำนวณ ค่าใดเป็นป้ายระบุ และค่าใดเป็นค่าคงที่ของตัวอย่าง
| สิ่งที่ประกาศ | ชนิดและค่าเริ่มต้นที่เลือก | เหตุผลเรื่องหน่วย/ส่วนเศษ | ผลที่คาดเมื่อแสดง | ผลจริง |
|---|---|---|---|---|
| จำนวนเต็มในตัวอย่าง | บันทึกเอง | บันทึกเอง | ก่อนรัน | หลังรัน |
| ค่าทศนิยม | บันทึกเอง | บันทึกเอง | ก่อนรัน | หลังรัน |
| อักขระหนึ่งตัว | บันทึกเอง | บันทึกเอง | ก่อนรัน | หลังรัน |
| ค่าคงที่ | บันทึกเอง | เหตุใดไม่ควรเปลี่ยนระหว่างงาน | ก่อนรัน | หลังรัน |
เมื่อให้เพื่อนตรวจ ให้เพื่อนอธิบายความหมายของชื่อตัวแปรก่อนดูผล หากชื่อไม่บอกหน่วยหรือหน้าที่ ให้ปรับชื่อพร้อมเหตุผล จากนั้นแปลและรันใหม่โดยตรวจว่าการปรับชื่อไม่ได้เปลี่ยนค่าที่คำนวณ
Lab B: นิพจน์และการแปลงชนิดข้อมูล
สถานการณ์ คำนวณพื้นที่วงกลมจากค่าคงที่พาย ตรวจผลของการหารจำนวนเต็มและเศษเหลือ และคำนวณการหารแบบทศนิยมด้วยการแปลงชนิด เพื่อฝึกเขียนและทำนายผลของนิพจน์
เป้าหมาย ฝึก LLO2.2 และ LLO2.3 คือเขียนนิพจน์และทำนายผลของการหารจำนวนเต็ม/เศษเหลือ และเขียนโปรแกรมคำนวณที่มีการแปลงชนิดให้คอมไพล์ผ่านและผลถูกต้อง
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch2_demo.cและพิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch2_demo.c : ตัวแปร ชนิดข้อมูล ค่าคงที่ และนิพจน์ */
#include <stdio.h>
int main(void) {
const double PI = 3.14159;
int radius = 5;
double area = PI * radius * radius;
int a = 7, b = 2;
int quotient = a / b;
int remainder = a % b;
double real_div = (double)a / b;
char grade = 'A';
printf("radius = %d, area = %.2f\n", radius, area);
printf("%d / %d = %d, %d %% %d = %d\n", a, b, quotient, a, b, remainder);
printf("(double)a / b = %.3f\n", real_div);
printf("grade = %c (code %d)\n", grade, grade);
return 0;
}- ก่อนรัน ให้ทำนายผลของแต่ละบรรทัดลงในตาราง โดยเฉพาะค่าของ
a / b,a % bและ(double)a / b
| นิพจน์ | ค่าที่ทำนาย | ค่าที่รันจริง |
|---|---|---|
PI * radius * radius | (เติมก่อนรัน) | 78.54 |
a / b | (เติมก่อนรัน) | 3 |
a % b | (เติมก่อนรัน) | 1 |
(double)a / b | (เติมก่อนรัน) | 3.500 |
- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch2_demo.c -o ch2_demo
./ch2_demoผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
radius = 5, area = 78.54
7 / 2 = 3, 7 % 2 = 1
(double)a / b = 3.500
grade = A (code 65)- เทียบผลที่ทำนายกับผลการรันจริง และอธิบายว่าเหตุใด
a / bจึงได้3ขณะที่(double)a / bได้3.500
ตัดสินใจเอง B.1 เพิ่มการคำนวณค่าเฉลี่ยของจำนวนเต็มสามจำนวนที่กำหนดเอง โดยให้ผลเป็นทศนิยมที่ถูกต้อง ต้องใช้การแปลงชนิดเพื่อไม่ให้ผลถูกตัดเศษ แล้วแสดงผลด้วย printf
ผลลัพธ์ที่คาดหวัง B.1 ค่าเฉลี่ยที่แสดงเป็นทศนิยม ไม่ถูกตัดเศษจากการหารจำนวนเต็ม และโปรแกรมคอมไพล์ผ่านโดยไม่มีการแจ้งเตือน
จุดตรวจ B คอมไพล์ผ่านไม่มีข้อผิดพลาดและไม่มีการแจ้งเตือน ผลลัพธ์ตรงกับที่ทำนาย และโค้ดมีคอมเมนต์ส่วนหัวและจัดย่อหน้า 4 ช่องว่าง
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| ค่าเฉลี่ยถูกตัดเศษเป็นจำนวนเต็ม | หารผลรวมจำนวนเต็มด้วยจำนวนเต็มโดยไม่แปลงชนิด | แปลงตัวตั้งเป็น double ก่อนหาร เช่น (double)(s1 + s2 + s3) / 3 |
| คอมไพล์แจ้งเตือนเกี่ยวกับตัวระบุรูปแบบ | ใช้ %d กับค่าชนิด double | ใช้ %.2f หรือ %.3f กับค่าทศนิยม |
ผล % ผิดพลาดหรือคอมไพล์ไม่ผ่าน | ใช้ % กับค่าทศนิยม | ใช้ % กับจำนวนเต็มเท่านั้น |
เปิดหลังผู้เรียนส่งงานแล้ว
ใบงานตรวจ Lab B: เปลี่ยนข้อมูลเพื่อแยกวิธีที่ถูกและผิด
ก่อนเทียบตารางผลตัวอย่าง ให้สร้างตารางคำทำนายของตนเองแยกไว้ ทดลองคู่ข้อมูลต่อไปนี้ด้วยสูตรเดิม แล้วจดชนิดที่ใช้ ณ จุดหารทุกครั้ง
ค่า a, b | a / b ที่คาด | a % b ที่คาด | (double)a / b ที่คาด | สิ่งที่ตรวจพบหลังรัน |
|---|---|---|---|---|
| 9, 4 | ก่อนรัน | ก่อนรัน | ก่อนรัน | อธิบายจุดที่มีเศษ |
| 8, 4 | ก่อนรัน | ก่อนรัน | ก่อนรัน | เหตุใดคู่หารลงตัวอาจซ่อนความผิด |
| 1, 4 | ก่อนรัน | ก่อนรัน | ก่อนรัน | แยกค่าศูนย์ที่คำนวณได้จากการอ่านข้อมูลผิด |
สำหรับงานต่อยอดค่าเฉลี่ย ใช้คะแนนสมมติ 7, 8 และ 10 เปรียบเทียบการหารจำนวนเต็ม การแปลงผลรวมก่อนหาร และการแปลงหลังหาร ให้ระบุว่าความต่างเกิดก่อนการจัดรูปแบบแสดงผล ไม่อธิบายเพียงว่าเปลี่ยนจำนวนหลักทศนิยมแล้วถูก
เก็บกวาด
เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น student_info และ ch2_demo) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด
ตรวจความเข้าใจก่อนเริ่มบทที่ 3
ตอบด้วยภาษาของตนเองโดยยกข้อมูลจากงานที่ทดลองจริง ไม่คัดคำตอบตัวอย่างมาแทนคำอธิบาย
- เหตุใดจำนวนสายกับน้ำหนักเฉลี่ยจึงเลือกชนิดต่างกัน แม้ต่างก็เป็นตัวเลข
- เมื่อ
aและbเป็นintการเขียนdouble result = a / b;รับรองว่าจะเก็บส่วนเศษหรือไม่ เพราะอะไร - ยกคู่ข้อมูลที่ช่วยพบตำแหน่ง cast ผิด และอธิบายว่าคู่หารลงตัวอาจพลาดความผิดนี้อย่างไร
- หลังคัดลอกค่าจากตัวแปรหนึ่งไปอีกตัว เหตุใดการแก้ตัวแรกจึงไม่เปลี่ยนตัวที่สองเอง
- หน่วยของตัวแปรกับชนิดของตัวแปรช่วยป้องกันความผิดคนละแบบอย่างไร
- ผล
sizeofที่ได้จากเครื่องหนึ่งบอกอะไรได้ และยังสรุปอะไรกับทุกเครื่องไม่ได้
การตรวจนี้เป็นกิจกรรมระหว่างเรียน ไม่เพิ่มคะแนนหรือเงื่อนไขผ่านจากแผนเดิม เก็บตารางคำทำนาย ผลจริง และเหตุผลที่แก้ไว้ใช้ต่อในบทที่ 3 การรับเข้าและการแสดงผล ซึ่งจะนำค่าที่ผู้ใช้ป้อนมาแทนค่าคงที่ในตัวอย่าง
สรุป
บทนี้อธิบายหน่วยพื้นฐานที่ใช้สร้างทุกโปรแกรม เริ่มจากตัวแปรและการประกาศด้วยรูปแบบ ชนิด ชื่อ = ค่า; ชนิดข้อมูลพื้นฐานสี่ชนิด (int, float, double, char) และการกำหนดค่าคงที่ด้วย const จากนั้นอธิบายตัวดำเนินการเลขคณิตและนิพจน์ โดยเน้นผลของการหารจำนวนเต็มที่ตัดเศษ ตัวดำเนินการหาเศษเหลือ ลำดับความสำคัญของตัวดำเนินการ และการแปลงชนิดข้อมูลด้วยการ cast เพื่อให้ผลลัพธ์เป็นทศนิยมที่ถูกต้อง ทักษะการเลือกชนิดข้อมูลและการเขียนนิพจน์ที่ถูกต้องในบทนี้เป็นพื้นฐานของการรับ-แสดงผลและโครงสร้างควบคุมในบทถัดไป
สิ่งที่ส่งต่อไปยังบทถัดไป
| สิ่งที่ส่งต่อ | ค่า/รูปแบบ | ใช้ในบท |
|---|---|---|
| ชนิดข้อมูลพื้นฐาน | int, float, double, char | บท 3 เป็นต้นไป |
| การประกาศตัวแปรและค่าคงที่ | ชนิด ชื่อ = ค่า; และ const | บท 3 เป็นต้นไป |
| ตัวดำเนินการเลขคณิตและลำดับความสำคัญ | + - * / % และวงเล็บ | บท 4–5 (เงื่อนไขและการวนซ้ำ) |
| การแปลงชนิดข้อมูล | (double)a / b | บท 3 (การจัดรูปแบบการแสดงผล) เป็นต้นไป |
สรุปคำสั่ง
| คำสั่ง | หน้าที่ |
|---|---|
gcc -std=c11 -Wall student_info.c -o student_info | คอมไพล์ซอร์สโค้ด student_info.c เป็นไฟล์ปฏิบัติการ พร้อมเปิดการแจ้งเตือน |
gcc -std=c11 -Wall ch2_demo.c -o ch2_demo | คอมไพล์ซอร์สโค้ด ch2_demo.c เป็นไฟล์ปฏิบัติการ พร้อมเปิดการแจ้งเตือน |
./student_info หรือ ./ch2_demo | รันไฟล์ปฏิบัติการที่คอมไพล์แล้ว |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
กลุ่ม ก (เข้าใจ)
- ก1 (LLO2.1) จงอธิบายความแตกต่างระหว่างชนิด int, double และ char พร้อมยกตัวอย่างข้อมูลที่เหมาะกับแต่ละชนิดอย่างละหนึ่งตัวอย่าง
- ก2 (LLO2.1) จงอธิบายว่าการประกาศค่าด้วย const ต่างจากตัวแปรธรรมดาอย่างไร และยกตัวอย่างค่าที่ควรประกาศเป็นค่าคงที่
กลุ่ม ข (ประยุกต์)
- ข1 (LLO2.2) กำหนดให้ a เป็น 17 และ b เป็น 5 จงทำนายผลของ
a / b,a % bและ(double)a / bแล้วเขียนโปรแกรมภาษา C ตรวจสอบ คอมไพล์และรันให้ได้ผลจริง - ข2 (LLO2.3) จงเขียนโปรแกรมภาษา C คำนวณพื้นที่สี่เหลี่ยมคางหมูจากความยาวด้านคู่ขนานสองด้านและความสูงที่กำหนดเป็นค่าในโปรแกรม โดยให้ผลเป็นทศนิยมที่ถูกต้อง แล้วแสดงผลด้วย
printf
กลุ่ม ค (วิเคราะห์)
- ค1 (LLO2.3) จากโปรแกรมที่คำนวณค่าเฉลี่ยของจำนวนเต็มสามจำนวนด้วย
int average = (s1 + s2 + s3) / 3;จงวิเคราะห์ว่าเหตุใดผลจึงอาจผิดพลาด และเสนอการแก้ไขด้วยการแปลงชนิดข้อมูลให้ถูกต้อง
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาชนิดข้อมูลพื้นฐานและตัวดำเนินการของภาษา C จาก Kernighan และ Ritchie (1988) บทที่ 2 แล้วสรุปความแตกต่างระหว่าง float และ double ด้วยถ้อยคำของตนเอง
- ทบทวนกฎการแปลงชนิดข้อมูลและลำดับความสำคัญของตัวดำเนินการจาก Kernighan และ Ritchie (1988) แล้วทดลองปรับโปรแกรม
ch2_demo.cให้คำนวณค่าเฉลี่ยของจำนวนเต็มหลายจำนวน - ทบทวนภาพรวมมาตรฐานภาษา C ตาม ISO/IEC 9899 และหน้า Arithmetic types ของ cppreference เพื่อเข้าใจขอบเขตและช่วงค่าต่ำสุดที่มาตรฐานรับประกันของชนิดข้อมูลพื้นฐาน
เอกสารอ้างอิง
cppreference.com. (2024). Arithmetic types — cppreference.com. สืบค้นจาก https://en.cppreference.com/w/c/language/arithmetic_types
cppreference.com. (2024). C operator precedence — cppreference.com. สืบค้นจาก https://en.cppreference.com/w/c/language/operator_precedence
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 ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว
แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 2
- GNU C: Division and Remainder — ตรวจการหารจำนวนเต็ม เศษ และการประกอบกลับ
- GNU C: Assignment Expressions — ตรวจความหมายของการกำหนดค่า
ตรวจเนื้อหาแหล่งข้อมูลเมื่อ 23 ตุลาคม 2569 แล้วเรียบเรียงคำอธิบายและสร้างตัวอย่างข้อมูลสมมติของบทนี้ ไม่ใช้ผลการทดลองเฉพาะเครื่องเป็นข้อกำหนดทั่วไปของ C11