Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 8 การจัดการสตริง
แผนบริหารการสอนประจำบทที่ 8
บทนี้ว่าด้วยการจัดการสตริง (string) ในภาษา C ซึ่งคือข้อความที่จัดเก็บเป็นอาร์เรย์ของอักขระ (char) ที่ปิดท้ายด้วยอักขระว่าง '\0' ต่อยอดจากอาร์เรย์ในบทที่ 7 โดยอธิบายว่าสตริงคืออาร์เรย์ของอักขระและเหตุที่ต้องมีอักขระว่างปิดท้าย การเข้าถึงและวนอักขระทีละตัวด้วยลูปจากบทที่ 5 การใช้ฟังก์ชันจัดการสตริงในส่วนหัว string.h ได้แก่ strlen strcpy strcmp และ strcat รวมถึงการรับสตริงทั้งบรรทัดที่มีช่องว่างด้วย fgets และการตัดอักขระขึ้นบรรทัดใหม่ด้วย strcspn เพื่อให้เขียนโปรแกรมรับและประมวลผลข้อความได้อย่างถูกต้องตามไวยากรณ์
| ผลลัพธ์ | ข้อความ | หลักฐาน | CLO |
|---|---|---|---|
| LLO8.1 | อธิบายว่าสตริงคืออาร์เรย์ของอักขระ (char) ที่ปิดท้ายด้วยอักขระว่าง และเข้าถึง/วนอักขระทีละตัวได้ | งานข้อ 1 (ปฏิบัติการที่ 8 Lab A) | CLO1 |
| LLO8.2 | ใช้ฟังก์ชันจัดการสตริงใน string.h (strlen, strcpy, strcmp, strcat) ได้ถูกต้อง | งานข้อ 2 (ปฏิบัติการที่ 8 Lab A) | CLO1 |
| LLO8.3 | เขียนโปรแกรมรับและประมวลผลสตริงได้ถูกต้องตามไวยากรณ์ รวมถึงการอ่านทั้งบรรทัดด้วย fgets | งานข้อ 3 (ปฏิบัติการที่ 8 Lab B) | CLO3 |
| LLO8.4 | ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้ | โค้ดทุกข้อที่ส่งในบทนี้ | CLO5 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO1 อธิบายและใช้ตัวแปร ชนิดข้อมูล การรับ-แสดงผล และโครงสร้างควบคุม (if/else, switch, loop) เพื่อแก้ปัญหาและได้ผลลัพธ์ที่ถูกต้อง
- CLO3 สามารถเขียนโค้ดได้ถูกต้องตามหลักไวยากรณ์ (Syntax) และมีประสิทธิภาพ (Efficiency)
บทที่ 8 นำแนวคิดอาร์เรย์จากบทที่ 7 มาเฉพาะเจาะจงกับอาร์เรย์ของอักขระ จึงเป็นสะพานเชื่อมไปสู่การเก็บข้อความเป็นเขตข้อมูลในโครงสร้างข้อมูลแบบระเบียน (struct) ในบทที่ 9 และการประมวลผลข้อความในบทต่อ ๆ ไป งานทั้งสามข้อในบทนี้สะสมหลักฐานของ CLO1 (งานข้อ 1–2) และ CLO3 (งานข้อ 3) ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท
วิธีสอน
- ทบทวนอาร์เรย์หนึ่งมิติและการเข้าถึงสมาชิกด้วยดัชนีจากบทที่ 7 แล้วตั้งคำถามว่าจะเก็บข้อความเช่นชื่อคนไว้ในตัวแปรอย่างไร เพื่อนำเข้าสู่แนวคิดว่าสตริงคืออาร์เรย์ของอักขระ
- อธิบายอักขระว่าง
'\0'ที่ปิดท้ายสตริง และสาธิตการประกาศchar name[20] = "Somchai";การแสดงผลด้วย%sและการเข้าถึงอักขระด้วยname[i]บนเครื่องจริง พร้อมย้ำว่าขนาดบัฟเฟอร์ต้องเผื่ออักขระว่างหนึ่งช่องเสมอ - สาธิตการวนอักขระด้วยลูป
forจนพบอักขระว่าง และเทียบความยาวที่นับเองกับค่าที่ได้จากstrlen - แนะนำฟังก์ชันใน
string.hได้แก่strlenstrcpystrcmpและstrcatโดยย้ำว่าต้อง#include <string.h>และstrlenคืนค่าชนิดsize_tที่พิมพ์ด้วย%zu - อธิบายการรับสตริงด้วย
scanf("%s", ...)ที่อ่านถึงช่องว่าง เทียบกับการอ่านทั้งบรรทัดด้วยfgetsและการตัดอักขระขึ้นบรรทัดใหม่ด้วยstrcspnแล้วให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
- เอกสารประกอบบทที่ 8 และสไลด์สรุปสตริง อักขระว่าง ฟังก์ชันใน
string.hและการรับสตริงด้วยfgets - ไฟล์โปรแกรมตัวอย่าง
ch8_string.cและch8_fgets.c
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 9 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 9 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | วนอักขระในสตริงจนพบอักขระว่าง และนับความยาวเอง แล้วเทียบกับ strlen | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO8.1 | CLO1 |
| 2 | ใช้ strlen strcpy strcmp และ strcat จัดการสตริงตามโจทย์ | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO8.2 | CLO1 |
| 3 | รับชื่อเต็มที่มีช่องว่างด้วย fgets ตัดอักขระขึ้นบรรทัดใหม่ แล้วแสดงผลและความยาว | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO8.3 | CLO3 |
เงื่อนไขผ่าน: งานข้อ 2 ประเมินจากไฟล์เดียวกับงานข้อ 1 ที่ต่อยอดการใช้ฟังก์ชันใน string.h ดังนั้นงานข้อ 1 ต้องวนอักขระถึงอักขระว่างและนับความยาวได้ถูกต้องและคอมไพล์ผ่านก่อนจึงจะประเมินข้อ 2 ได้
คะแนนใบงาน (10 คะแนน)
| ผลลัพธ์ | สิ่งที่ตรวจ | คะแนน |
|---|---|---|
| LLO8.1 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO8.2 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO8.3 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO8.4 | มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง | 1 |
ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา
เกณฑ์ให้คะแนน
งานข้อ 1 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:
| เกณฑ์ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| งานข้อ 1 | วนอักขระจนพบอักขระว่าง และความยาวที่นับเองตรงกับ strlen | วนไม่ถึงอักขระว่าง หรือความยาวที่นับไม่ตรงกับ strlen |
งานข้อ 2 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ (checklist): #include <string.h> ครบ, เรียก strlen strcpy strcmp strcat ถูกต้องและผลตรง, ใช้ %zu กับ strlen, จัดย่อหน้า 4 ช่องว่างและมีคอมเมนต์ส่วนหัว
งานข้อ 3 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ: ใช้ fgets รับทั้งบรรทัดถูกต้อง, ตัดอักขระขึ้นบรรทัดใหม่ด้วย strcspn, บัฟเฟอร์ใหญ่พอ, คอมไพล์ผ่านไม่มีการแจ้งเตือน, เป็นไปตามมาตรฐานโค้ด
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
บทนำ
โปรแกรมจำนวนมากต้องจัดเก็บและประมวลผลข้อความ เช่น ชื่อคน ที่อยู่ หรือคำสั่งที่ผู้ใช้พิมพ์เข้ามา ภาษา C ไม่มีชนิดข้อมูลสตริงแยกต่างหาก แต่ใช้อาร์เรย์ของอักขระ (char) จากบทที่ 7 เก็บข้อความ โดยปิดท้ายด้วยอักขระว่าง '\0' เพื่อบอกจุดสิ้นสุดของสตริง ด้วยเหตุนี้สตริงจึงเป็นการนำอาร์เรย์มาใช้ในกรณีเฉพาะ และนำลูปจากบทที่ 5 มาวนอักขระ ฟังก์ชันจากบทที่ 6 มาจัดการข้อความ และการรับเข้า/แสดงผลจากบทที่ 3 มารับและแสดงข้อความได้โดยตรง
ภาษา C เตรียมฟังก์ชันจัดการสตริงที่ใช้บ่อยไว้ในส่วนหัว string.h เช่น การหาความยาว การคัดลอก การเปรียบเทียบ และการต่อสตริง จึงไม่ต้องเขียนเองทุกครั้ง บทนี้อธิบายทั้งการใช้ฟังก์ชันเหล่านี้และการวนอักขระด้วยตนเอง รวมถึงการรับข้อความทั้งบรรทัดที่มีช่องว่างด้วย fgets ซึ่งปลอดภัยและยืดหยุ่นกว่าการใช้ scanf("%s", ...) ทักษะเหล่านี้ใช้ต่อในการเก็บข้อความเป็นเขตข้อมูลของโครงสร้างข้อมูลแบบระเบียนในบทที่ 9 และในบทต่อไป
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
สตริงคืออาร์เรย์ของอักขระปิดท้ายด้วย '\0' และการวนอักขระจนพบอักขระว่าง | บท 9 (ชื่อใน struct) และบทที่ประมวลผลข้อความ |
ฟังก์ชันใน string.h (strlen, strcpy, strcmp, strcat) และ %s, %zu | บท 9 เป็นต้นไป |
การรับทั้งบรรทัดด้วย fgets และการตัดอักขระขึ้นบรรทัดใหม่ด้วย strcspn | ทุกบทที่รับข้อความจากผู้ใช้ |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE และต้องมีความรู้เรื่องอาร์เรย์หนึ่งมิติและการเข้าถึงด้วยดัชนีจากบทที่ 7 การวนซ้ำด้วยลูป for จากบทที่ 5 การออกแบบฟังก์ชันจากบทที่ 6 และการรับเข้า/แสดงผลด้วย scanf/printf จากบทที่ 3 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ผลการแสดงข้อความภาษาไทยอาจต่างกันเล็กน้อย แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน
วิธีใช้บทเรียนและใบงานในหน้านี้
ทบทวน บทที่ 7 อาร์เรย์ โดยแยกพื้นที่ที่จองไว้กับจำนวนข้อมูลจริง บทนี้เพิ่มตัวปิดสตริง '\0' ซึ่งบอกจุดหยุดของฟังก์ชันข้อความ แต่ไม่ได้บอกความจุของอาร์เรย์ให้ฟังก์ชันรู้
ตารางใบงานเป็นแบบสำหรับคัดลงสมุดหรือเอกสารของตน ไม่ใช่ช่องส่งคำตอบบนเว็บไซต์ ใช้ลำดับ วาดช่อง → ทำนาย → คอมไพล์ → รัน → อธิบายความต่าง พร้อมเก็บผลที่คาดไว้ก่อนแก้
ใช้รหัสและชื่อสมมติ กิจกรรมเพิ่มไม่เปลี่ยน LLO8.1–8.3 งานส่ง หรือสัดส่วนคะแนน เวลาและเฉลยอยู่เฉพาะส่วนผู้สอน โปรแกรมอ่านบรรทัดในบทนี้รับข้อความที่ไม่มี null byte แฝง ไม่ใช่ตัวอ่านข้อมูลไบนารีทั่วไป
8.1 สตริงและตัวปิดสตริง
ระดับที่ 1: สตริง (string) ในภาษา C คือข้อความที่จัดเก็บเป็นอาร์เรย์ของอักขระ (char) จากบทที่ 7 โดยปิดท้ายด้วยอักขระว่าง '\0' (null terminator) ที่ทำหน้าที่บอกจุดสิ้นสุดของข้อความ ภาษา C ไม่มีชนิดข้อมูลสตริงแยกต่างหาก จึงใช้อาร์เรย์ของอักขระแทน
ระดับที่ 2: การประกาศสตริงทำได้ด้วยอาร์เรย์ของอักขระพร้อมกำหนดค่าเริ่มต้นเป็นข้อความในเครื่องหมายอัญประกาศคู่ เช่น char name[20] = "Somchai"; การแสดงผลสตริงทั้งข้อความใช้ตัวระบุรูปแบบ %s กับ printf และการเข้าถึงอักขระทีละตัวใช้ดัชนีเหมือนอาร์เรย์ทั่วไป เช่น name[0] คืออักขระ 'S'
char name[20] = "Somchai";
printf("%s\n", name);
printf("%c\n", name[0]);ระดับที่ 3: ข้อความ "Somchai" มี 7 อักขระ แต่ใช้พื้นที่ในอาร์เรย์ 8 ช่อง เพราะต้องเก็บอักขระว่าง '\0' ปิดท้ายอีกหนึ่งช่อง ด้วยเหตุนี้ขนาดบัฟเฟอร์ของสตริงจึงต้องไม่น้อยกว่าจำนวนไบต์ของเนื้อหาบวกหนึ่ง อักขระว่างนี้สำคัญเพราะฟังก์ชันจัดการสตริงและการแสดงผลด้วย %s อาศัยอักขระว่างเป็นจุดหยุด หากไม่มีอักขระว่างปิดท้าย การวนหรือการแสดงผลจะเลยขอบเขตข้อความออกไปจนได้ผลลัพธ์ที่คาดเดาไม่ได้
คำถามตรวจความเข้าใจ 8.1: สตริง "Hi" ใช้พื้นที่กี่ช่องในอาร์เรย์ char เพราะเหตุใด
ใบงาน 1: พื้นที่ ข้อความว่าง และอักขระศูนย์
สร้างไฟล์ string_layout.c เพื่อเทียบขนาดอาร์เรย์กับความยาวสตริง ทุกอาร์เรย์ในตัวอย่างตั้งค่าไว้ครบส่วนที่นำมาอ่าน และมีตัวปิดในพื้นที่จริง
/* string_layout.c : capacity is different from string length */
#include <stdio.h>
#include <string.h>
int main(void)
{
char code[5] = "0017";
char empty[] = "";
char zero_text[] = "0";
char sample[5] = {'A', '\0', 'B', 'C', '\0'};
printf("code capacity=%zu bytes=%zu\n", sizeof code, strlen(code));
printf("empty capacity=%zu bytes=%zu\n", sizeof empty, strlen(empty));
printf("zero_text bytes=%zu\n", strlen(zero_text));
printf("sample bytes=%zu after_zero=%c%c\n",
strlen(sample), sample[2], sample[3]);
return 0;
}| สิ่งที่ดู | ความจุ | strlen ที่ทำนาย | เหตุผลที่หยุด |
|---|---|---|---|
| code | |||
| empty | |||
| zero_text | |||
| sample |
แยก '0' ซึ่งเป็นอักขระตัวเลขออกจาก '\0' ซึ่งมีค่าเป็นศูนย์ การพบตัวปิดไม่ได้แปลว่าพื้นที่ด้านหลังหายไป แต่ฟังก์ชันสตริงจะไม่ถือว่าไบต์หลังตัวปิดแรกเป็นส่วนของข้อความนั้น
ใบงาน 2: วนถึงตัวปิดโดยไม่นับตัวปิด
วาด "Hi" เป็นสามช่องพร้อมดัชนี แล้วเติมตารางของลูปที่ทดสอบ text[i] != '\0' ก่อนพิมพ์และเพิ่มตัวนับ ใช้ชื่ออักขระแทนการเขียนค่า ASCII เพื่อไม่ผูกเหตุผลกับชุดรหัสหนึ่งโดยไม่จำเป็น
| i | สิ่งที่อยู่ในช่อง | เงื่อนไขจริงหรือเท็จ | พิมพ์หรือไม่ | ตัวนับหลังทำ |
|---|---|---|---|---|
| 0 | ||||
| 1 | ||||
| 2 |
ทำซ้ำกับสตริงว่างและ "0017" จำนวนรอบทำงานต้องตรงกับ strlen ไม่ใช่ sizeof อาร์เรย์ ลูปที่ใช้ sizeof กับบัฟเฟอร์ใหญ่กว่าข้อความอาจอ่านส่วนที่ไม่ใช่ข้อมูลข้อความ
ก่อนใช้ strlen หรือ %s ต้องมีสตริงที่ปิดครบในพื้นที่ที่อ่านได้ การมี char array ที่ยังไม่ตั้งค่าไม่ทำให้มันเป็นสตริงโดยอัตโนมัติ และไม่ใช้การรันนอกขอบเขตเพื่อเดาว่าตัวปิดอยู่ที่ใด
8.2 ฟังก์ชันจัดการสตริงใน string.h
ระดับที่ 1: ภาษา C เตรียมฟังก์ชันจัดการสตริงที่ใช้บ่อยไว้ในส่วนหัว string.h การใช้ฟังก์ชันเหล่านี้ต้องเพิ่ม #include <string.h> ที่ต้นไฟล์เสมอ
ระดับที่ 2: ฟังก์ชันที่ใช้บ่อยมีสี่ฟังก์ชัน strlen(s) คืนความยาวของสตริงเป็นจำนวนไบต์ ไม่รวมอักขระว่าง เป็นชนิด size_t ซึ่งพิมพ์ด้วยตัวระบุรูปแบบ %zu · strcpy(ปลายทาง, ต้นทาง) คัดลอกสตริงต้นทางไปยังปลายทาง · strcmp(a, b) เปรียบเทียบสองสตริงและคืนค่า 0 เมื่อสองสตริงเท่ากัน · strcat(ปลายทาง, ต้นทาง) ต่อสตริงต้นทางเข้าท้ายปลายทาง
#include <string.h>
char copy[20];
strcpy(copy, name);
printf("ความยาว = %zu\n", strlen(name));ระดับที่ 3: strcpy และ strcat ไม่ตรวจขนาดของอาร์เรย์ปลายทางให้ จึงต้องมั่นใจว่าปลายทางมีพื้นที่ใหญ่พอรองรับข้อความผลลัพธ์รวมอักขระว่างปิดท้าย ตัวอย่างเช่น การต่อ "Somchai" เข้าท้าย "Hello, " ได้ผลยาว 14 อักขระ จึงต้องประกาศปลายทางขนาดอย่างน้อย 15 ช่อง หากปลายทางเล็กเกินไป การเขียนจะเลยขอบเขตอาร์เรย์และทำให้โปรแกรมทำงานผิดพลาด เพราะภาษา C ไม่ตรวจขอบเขตนี้ให้
คำถามตรวจความเข้าใจ 8.2: strcmp คืนค่าใดเมื่อสตริงสองตัวเท่ากัน
ใบงาน 3: เปรียบเทียบเนื้อหา ไม่ตีความเป็นจำนวน
ทำนายเฉพาะเครื่องหมายของผล strcmp ได้แก่ ติดลบ ศูนย์ หรือบวก ไม่ต้องทายตัวเลขผลต่างที่คอมไพเลอร์หนึ่งแสดง และอย่าตรวจด้วย == 1 หรือ == -1
| สตริงซ้าย | สตริงขวา | เครื่องหมายของผลที่ทำนาย | จุดแรกที่ต่าง |
|---|---|---|---|
"0017" | "0017" | ||
"0017" | "0018" | ||
"0018" | "0017" | ||
"0017" | "17" | ||
"" | "0017" |
การคัดลอกแล้วเท่ากันตรวจด้วย strcmp ไม่ใช่การเปรียบเทียบที่อยู่ของอาร์เรย์ รหัส "0017" เก็บศูนย์นำหน้าไว้ และการเรียงรหัสที่มีความกว้างต่างกันไม่ใช่การเรียงค่าจำนวนเต็มโดยอัตโนมัติ
8.3 การรับสตริงและการวนอักขระ
ระดับที่ 1: การรับสตริงจากผู้ใช้ทำได้ด้วย scanf("%s", ...) จากบทที่ 3 แต่วิธีนี้อ่านข้อความถึงช่องว่างแรกเท่านั้น จึงรับได้เพียงคำเดียว ส่วนการอ่านทั้งบรรทัดที่อาจมีช่องว่างใช้ fgets
ระดับที่ 2: fgets(buf, ขนาด, stdin) อ่านได้ไม่เกินขนาดลบหนึ่งไบต์ โดยหยุดเมื่ออ่าน newline หรือถึง EOF จึงอาจได้เพียงส่วนหนึ่งของบรรทัดเมื่อพื้นที่ไม่พอ แต่โดยปกติจะรวมอักขระขึ้นบรรทัดใหม่ '\n' ที่ท้ายบรรทัดมาด้วย จึงนิยมตัดทิ้งด้วย strcspn ซึ่งคืนตำแหน่งของอักขระที่พบ แล้วแทนที่ตำแหน่งนั้นด้วยอักขระว่าง ดังบรรทัด line[strcspn(line, "\n")] = '\0';
char line[50];
if (fgets(line, sizeof(line), stdin) != NULL) {
line[strcspn(line, "\n")] = '\0';
}ระดับที่ 3: การวนอักขระในสตริงทีละตัวทำได้โดยวนลูป for จนพบอักขระว่าง ด้วยเงื่อนไข for (int i = 0; s[i] != '\0'; i++) ซึ่งเป็นวิธีนับความยาวหรือประมวลผลอักขระด้วยตนเองโดยไม่พึ่ง strlen การวนแบบนี้อาศัยอักขระว่างเป็นจุดหยุดเช่นเดียวกับฟังก์ชันมาตรฐาน หากสตริงไม่มีอักขระว่างปิดท้าย การวนจะไม่หยุดที่ปลายข้อความ
คำถามตรวจความเข้าใจ 8.3: เหตุใดการอ่านชื่อที่มีช่องว่างจึงใช้ scanf("%s", ...) ไม่ได้
8.4 ข้อควรระวังเรื่องขนาดและความปลอดภัย
ระดับที่ 1: บัฟเฟอร์ของสตริงต้องมีขนาดใหญ่พอเก็บข้อความที่ยาวที่สุดที่คาดว่าจะรับ บวกอีกหนึ่งช่องสำหรับอักขระว่างปิดท้ายเสมอ
ระดับที่ 2: หากข้อความที่รับหรือที่ต่อยาวเกินขนาดบัฟเฟอร์ การเขียนจะเลยขอบเขตอาร์เรย์ (buffer overflow) ซึ่งเป็นข้อผิดพลาดที่ภาษา C ไม่ตรวจให้และอาจทำให้โปรแกรมพังหรือได้ผลที่คาดเดาไม่ได้ ด้วยเหตุนี้การเลือกขนาดบัฟเฟอร์จึงต้องเผื่อให้เพียงพอตั้งแต่ประกาศ
ระดับที่ 3: เมื่อรับสตริงด้วย scanf สามารถจำกัดความกว้างได้ด้วยตัวระบุเช่น %19s ที่อ่านได้ไม่เกิน 19 อักขระสำหรับบัฟเฟอร์ขนาด 20 ช่อง เพื่อเว้นที่ให้อักขระว่าง อย่างไรก็ตามการรับข้อความที่อาจมีช่องว่างและอาจยาว การใช้ fgets ที่ระบุขนาดบัฟเฟอร์เป็นอาร์กิวเมนต์ย่อมปลอดภัยและยืดหยุ่นกว่า scanf("%s", ...) เพราะควบคุมจำนวนอักขระที่อ่านได้และรับทั้งบรรทัด
คำถามตรวจความเข้าใจ 8.4: เหตุใดบัฟเฟอร์ของสตริงจึงต้องมีขนาดมากกว่าจำนวนอักขระของข้อความอย่างน้อยหนึ่งช่อง
ใบงานสัญญาข้อความ: ตอบก่อนเลือกขนาดบัฟเฟอร์
| ช่องข้อมูลสมมติ | ความจุเป็นไบต์ | เนื้อหาสูงสุดไม่รวมตัวปิด | ยอมรับช่องว่างหรือไม่ | ยอมรับข้อความว่างหรือไม่ | เมื่อยาวเกิน |
|---|---|---|---|---|---|
| รหัสสี่หลัก | |||||
| ชื่ออุปกรณ์ | |||||
| ข้อความแสดงผลที่ต่อแล้ว |
ระบุข้อกำหนดก่อนใช้ strcpy หรือ strcat เพราะฟังก์ชันเหล่านี้ไม่ทราบพื้นที่ปลายทางเอง การเพิ่มขนาดเพียงครั้งเดียวไม่ตอบว่าจะทำอย่างไรกับข้อความที่ยาวกว่าขนาดใหม่
แยกข้อความว่างที่อ่าน newline สำเร็จออกจาก EOF ก่อนอ่านได้แม้แต่ไบต์เดียว ทั้งสองกรณีอาจไม่มีเนื้อหาให้ใช้ แต่สถานะข้อมูลเข้าต่างกัน และไม่ควรพิมพ์บัฟเฟอร์ที่อ่านไม่สำเร็จ
8.5 กรณีศึกษาต่อเนื่อง: รหัสอุปกรณ์ที่ขึ้นต้นด้วยศูนย์
รหัส "0017" ไม่เหมือนจำนวน 17 ในเชิงการแสดงตัวตน แม้จะมีแต่ตัวเลขก็ตาม ถ้านำรหัสไปอ่านเป็นจำนวนเต็มแล้วพิมพ์กลับ เลขศูนย์นำหน้าจะหาย การเก็บสตริงจึงช่วยรักษารูปแบบข้อมูลที่ไม่ได้มีไว้คำนวณ เช่น รหัสตัวอย่าง ชื่ออุปกรณ์ และข้อความอธิบาย
8.5.1 วาดช่องเก็บรวมตัวปิดสตริง
char code[5] = "0017"; มีสมาชิกห้าช่อง คือ '0', '0', '1', '7' และ '\0' สี่ช่องแรกเป็นเนื้อหา ช่องสุดท้ายบอกจุดจบให้ฟังก์ชันสตริงทราบ ความยาวเนื้อหาเป็นสี่ แต่ความจุที่ต้องมีอย่างน้อยเป็นห้า
GNU C: Strings อธิบายการเผื่อพื้นที่สำหรับ null terminator สตริงที่ใช้กับฟังก์ชันมาตรฐานต้องมีตัวปิดอยู่ภายในพื้นที่ที่อ่านได้ การมีอาร์เรย์ char เพียงอย่างเดียวไม่ได้รับรองว่าเป็นสตริงที่สมบูรณ์
| รูปแบบ | เนื้อหาที่ต้องการ | พื้นที่ขั้นต่ำ |
|---|---|---|
"" | ข้อความว่าง | 1 ช่องสำหรับตัวปิด |
"A" | ASCII หนึ่งตัว | 2 ช่อง |
"0017" | รหัสสี่ตัว | 5 ช่อง |
| สตริง ASCII ยาว 19 ตัว | เนื้อหา 19 ตัว | 20 ช่อง |
8.5.2 ความยาวเป็นไบต์ ไม่ใช่จำนวนตัวอักษรที่ผู้อ่านเห็น
strlen นับจำนวนไบต์ก่อนตัวปิดสตริง ไม่รวมตัวปิด ในตัวอย่าง ASCII หนึ่งตัวใช้หนึ่งไบต์จึงดูเหมือนนับตัวอักษรได้ตรง แต่ข้อความภาษาไทยที่เข้ารหัส UTF-8 ใช้หลายไบต์ต่อจุดรหัส และสระหรือวรรณยุกต์อาจประกอบกันเป็นหน่วยที่ผู้อ่านมองเป็นหนึ่งตัวอักษร
ดังนั้นไม่ควรประกาศว่าบัฟเฟอร์ 20 ช่องเก็บชื่อภาษาไทยได้ 19 ตัวอักษรเสมอ และการวน char ทีละช่องไม่เท่ากับการตัดคำหรือกลับลำดับตัวอักษรไทย บทนี้ฝึกกับ ASCII เพื่อให้เห็นกลไกสตริงก่อน การรองรับภาษาธรรมชาติหลายภาษาต้องมีการออกแบบเพิ่มเติม
8.6 รับข้อความทั้งบรรทัดโดยรู้ว่าอะไรยังอยู่ใน input
#include <stdio.h>
#include <string.h>
int main(void)
{
char name[20];
if (fgets(name, sizeof name, stdin) == NULL) {
puts("Input error");
return 1;
}
size_t newline = strcspn(name, "\n");
if (name[newline] == '\n') {
name[newline] = '\0';
} else if (strlen(name) == sizeof name - 1) {
puts("Line may be too long; not accepted");
return 1;
}
printf("name=[%s] bytes=%zu\n", name, strlen(name));
return 0;
}ป้อน Cable kit แล้วขึ้นบรรทัดใหม่ จะได้ name=[Cable kit] bytes=9 ฟังก์ชัน fgets รับช่องว่างได้ และเก็บ newline เมื่ออ่านมาถึง newline ภายในขนาดที่กำหนด จึงต้องตัดเฉพาะเมื่อพบจริง strcspn ช่วยหาตำแหน่งแรกที่พบอักขระในชุดที่กำหนด หรือคืนความยาวเมื่อไม่พบ
โค้ดนี้ตั้งใจปฏิเสธแบบระมัดระวังเมื่อบัฟเฟอร์เต็มแต่ยังไม่เห็น newline รวมถึงบางกรณีที่ข้อความพอดี 19 ไบต์ ตัวอย่างยอมเสียความยืดหยุ่นเพื่อไม่รับเพียงส่วนต้นของชื่อโดยเงียบ ๆ และจบหลังการปฏิเสธ จึงไม่ต้องกู้ input เพื่ออ่านรายการถัดไป สัญญาของตัวอย่างคือข้อความจากแป้นพิมพ์ไม่มี null byte แฝง ไม่ใช่ตัวอ่านแฟ้มข้อมูลไบนารี
หากต้องรับต่อหลังพบข้อความยาว ต้องอ่านส่วนที่เหลือจนถึง newline หรือจบข้อมูลก่อนเริ่มรายการใหม่ ไม่เช่นนั้นส่วนปลายของชื่อเดิมอาจกลายเป็นคำตอบของคำถามถัดไป ข้อเท็จจริงเรื่องขนาด การเก็บ newline และค่าคืนของ fgets ตรวจได้จาก glibc: Line-Oriented Input
8.6.1 การผสม scanf กับ fgets ต้องอธิบายข้อมูลค้าง
หลัง scanf("%d", &count) อ่านเลขสำเร็จ newline อาจยังไม่ถูกอ่าน เมื่อเรียก fgets ถัดไปจึงอาจได้เพียง newline แทนชื่อ อย่าแก้ด้วย fflush(stdin) เพราะไม่ใช่วิธีมาตรฐาน C11 สำหรับล้างข้อมูลเข้า และอย่าใส่ getchar หนึ่งครั้งโดยไม่รู้ว่ามีข้อมูลเหลือกี่ตัว
ในงานที่ต้องอ่านหลายช่องอย่างจริงจัง แนวทางอ่านทีละบรรทัดแล้วแยกวิเคราะห์ทำให้ขอบเขตรายการชัดขึ้น แต่ต้องตรวจการแปลงค่าอย่างครบถ้วน ในบทนี้ให้ใช้รูปแบบรับข้อมูลตามตัวอย่างแต่ละงานอย่างสม่ำเสมอและอธิบายข้อจำกัด แทนการผสมหลายฟังก์ชันแล้วแก้เฉพาะอาการ
ตัวอย่างพร้อมใช้: อ่านบรรทัดพอดีขนาดและกู้ส่วนที่ยาวเกิน
สร้างไฟล์ read_two_lines.c โดยใช้บัฟเฟอร์ 8 ไบต์ รับเนื้อหาได้ไม่เกิน 7 ไบต์ ฟังก์ชันอ่านต่อหนึ่งไบต์เมื่อพื้นที่เต็ม เพื่อแยกข้อความพอดีจากข้อความยาวเกิน แล้วทิ้งส่วนที่เกินก่อนอ่านรายการถัดไป
/* read_two_lines.c : full lines, not silently truncated prefixes */
#include <stdio.h>
#include <string.h>
int read_line(char line[]);
int main(void)
{
char line[8];
for (int number = 1; number <= 2; ++number) {
int status = read_line(line);
if (status == 0) {
puts("End of input");
break;
}
if (status == -2) {
puts("Input error");
return 1;
}
if (status == -1) {
printf("line%d: Too long\n", number);
continue;
}
printf("line%d=[%s] bytes=%zu\n", number, line, strlen(line));
}
return 0;
}
/* Requires room for 8 bytes and text without embedded null bytes.
1: accepted, 0: no line at EOF, -1: too long, -2: input error. */
int read_line(char line[])
{
line[0] = '\0';
if (fgets(line, 8, stdin) == NULL) {
return ferror(stdin) ? -2 : 0;
}
size_t end = strcspn(line, "\n");
if (line[end] == '\n') {
line[end] = '\0';
return 1;
}
if (strlen(line) < 7) {
return 1;
}
int ch = getchar();
if (ch == '\n') {
return 1;
}
if (ch == EOF) {
return ferror(stdin) ? -2 : 1;
}
while ((ch = getchar()) != '\n' && ch != EOF) {
}
line[0] = '\0';
return ferror(stdin) ? -2 : -1;
}| ข้อมูลที่จะลอง | ประเด็นที่ทำนาย | ผลจริง |
|---|---|---|
บรรทัดว่าง แล้ว OK | อ่านสำเร็จแต่ยาวศูนย์ | |
1234567 แล้ว OK | เนื้อหาพอดีเจ็ดไบต์ | |
12345678 แล้ว OK | ปฏิเสธบรรทัดแรกและไม่ปนบรรทัดสอง | |
Cable แล้ว EOF ไม่มี newline | เก็บตัว e ไว้ | |
| EOF ทันที | ไม่มีบรรทัดให้นำไปแสดง |
ชุดทดลองใช้ ASCII และ newline แบบ LF ตัวอ่านตรวจไบต์ ไม่ได้ตรวจความสมบูรณ์ของ UTF-8 หรือจัดการรูปแบบไฟล์ทุกชนิด ถ้ามี carriage return ที่สตรีมไม่ได้แปลงให้ จะถือเป็นเนื้อหาตามกติกาปัจจุบัน
เทียบกับตัวอย่าง name[20] เดิมซึ่งปฏิเสธอย่างระมัดระวังเมื่อเต็มและไม่มี newline ตัวอย่างใหม่นี้เพิ่มขั้นอ่านล่วงหน้า จึงยอมรับเนื้อหาพอดีเจ็ดไบต์ได้ และอ่านต่อหลังปฏิเสธโดยไม่ใช้ส่วนปลายเป็นชื่อใหม่
ใบงาน input ค้าง: วาดสิ่งที่ยังไม่ได้อ่าน
สมมติ input เป็น 12\nCable kit\n หลัง scanf อ่านจำนวนเต็ม 12 สำเร็จ ให้ขีดส่วนที่ถูกอ่านแล้ว หากเรียก fgets ทันที บรรทัดใดจะถูกอ่านก่อน และข้อความที่ได้หลังตัด newline เป็นอะไร
| ขั้น | สิ่งที่อ่านไปแล้ว | สิ่งที่เหลือในสตรีม | ผลที่คาด |
|---|---|---|---|
| ก่อนอ่านเลข | |||
| หลังอ่านเลขสำเร็จ | |||
| หลัง fgets ครั้งแรก | |||
| หลัง fgets ครั้งที่สอง |
ทำซ้ำกับ 12 \nCable kit\n เพื่อเห็นว่าข้อมูลค้างไม่จำเป็นต้องมีเพียงหนึ่งตัว อย่าเพิ่ม getchar ครั้งเดียวหรือ fflush(stdin) โดยไม่กำหนดว่าจะทิ้งข้อมูลใด
การออกแบบรับทุกช่องเป็นบรรทัดแล้วแปลงข้อมูลภายหลังช่วยให้ขอบเขตแต่ละคำตอบชัด แต่การแปลงเลขยังต้องตรวจความสำเร็จและข้อมูลที่เหลือ บทนี้เน้นเข้าใจสถานะสตรีมก่อนเพิ่มตัวแปลงทั่วไป
8.7 เปรียบเทียบ คัดลอก และเวิร์กช็อปตรวจขนาด
strcmp(a, b) คืนค่าติดลบ ศูนย์ หรือบวกตามลำดับการเปรียบเทียบ ไม่รับประกันว่าจะได้เพียง -1 หรือ 1 จึงเขียนเงื่อนไขตรวจน้อยกว่าศูนย์ มากกว่าศูนย์ หรือเท่าศูนย์ ไม่ใช้ a == b เพื่อเปรียบเทียบเนื้อหาของอาร์เรย์สตริงสองชุด
ก่อนคัดลอกต้องรู้ความจุปลายทางและความยาวต้นทาง ถ้าความยาวต้นทางเท่าความจุปลายทางพอดี ยังไม่มีช่องสำหรับตัวปิด จึงไม่พอ การเปลี่ยนจาก strcpy เป็น strncpy โดยไม่อ่านสัญญาฟังก์ชันไม่ใช่การแก้ที่สมบูรณ์ เพราะบางกรณี strncpy ไม่เติมตัวปิด ต้องออกแบบการปฏิเสธหรือตัดข้อความให้ชัดและแจ้งผู้ใช้ตามข้อกำหนด
เวิร์กช็อปแรกวาดผังช่องของสตริง "", "A" และ "0017" ระบุทั้ง sizeof อาร์เรย์และ strlen แล้วอธิบายว่าทำไมตัวเลขต่างกัน เวิร์กช็อปที่สองรับชื่ออุปกรณ์ที่มีช่องว่าง ทดสอบข้อความว่าง ชื่อสั้น และชื่อเกินความจุ โดยตรวจว่าข้อความยาวไม่ถูกยอมรับเป็นอีกชื่อหนึ่ง
เวิร์กช็อปที่สามใช้รหัสสมมติ "0017", "0018" และ "0100" เปรียบเทียบเพื่อเลือกค่าที่ตรงกับรหัสเป้าหมาย ให้แยกการเปรียบเทียบเนื้อหากับการเปรียบเทียบตัวเลข จากนั้นอธิบายว่าการเรียงรหัสตามตัวอักษรจะให้ลำดับเหมือนจำนวนได้ภายใต้เงื่อนไขใด เช่น มีแต่เลขและความกว้างเท่ากัน
ปิดท้ายด้วยการบันทึกข้อตกลงของทุกช่องข้อความ ได้แก่รับได้กี่ไบต์ รับช่องว่างหรือไม่ ยอมรับข้อความว่างหรือไม่ และทำอย่างไรเมื่อยาวเกิน จุดตรวจนี้จะใช้ต่อกับชื่อและรหัสใน struct และโครงงานโดยตรง
ตัวอย่างตรวจความจุ: ปฏิเสธก่อนต่อและคงข้อความเดิม
สร้างไฟล์ checked_append.c เริ่มด้วยรหัส "0017" และต่อ "-kit" พื้นที่จริงมี 12 ไบต์ แต่ให้ป้อนความจุที่อนุญาตช่วง 5–12 เพื่อเห็นว่าตัวเลขที่ส่งให้ฟังก์ชันต้องไม่เกินพื้นที่จริง
/* checked_append.c : capacity includes the null terminator */
#include <stdio.h>
#include <string.h>
int append_text(char destination[], size_t capacity, const char suffix[]);
int main(void)
{
char code[12] = "0017";
int capacity;
if (scanf("%d", &capacity) != 1) {
puts("Input error");
return 1;
}
if (capacity < 5 || capacity > 12) {
puts("Range error");
return 1;
}
printf("before=[%s]\n", code);
int accepted = append_text(code, (size_t)capacity, "-kit");
puts(accepted ? "Appended" : "Not enough room");
printf("after=[%s] bytes=%zu\n", code, strlen(code));
return 0;
}
/* Both strings must be terminated, readable, and non-overlapping.
capacity must not exceed the actual destination storage. */
int append_text(char destination[], size_t capacity, const char suffix[])
{
size_t used = strlen(destination);
size_t extra = strlen(suffix);
if (used >= capacity || extra > capacity - used - 1) {
return 0;
}
strcat(destination, suffix);
return 1;
}ทำนายเมื่อความจุเป็น 5, 8, 9 และ 12 โดยวาดช่องเนื้อหาเดิม เนื้อหาใหม่ และตัวปิด ผลข้อความ "0017-kit" ยาวแปดไบต์ จึงต้องมีเก้าช่อง ไม่ใช่แปด
ฟังก์ชันตรวจความจุก่อนเปลี่ยนข้อมูล และไม่ใช้การตัดข้อความโดยเงียบ สัญญาต้องรับรองว่าต้นทางทั้งสองเป็นสตริงที่ปิดแล้ว เพราะ strlen ไม่สามารถใช้ตรวจว่าพื้นที่สุ่มที่ไม่มีตัวปิดปลอดภัยหรือไม่
8.8 ตัวอย่างเปรียบเทียบ: ตัด newline กับตัดข้อมูลจริง
สมมติบัฟเฟอร์รับ "Cable\n" มีเนื้อหาอักษรห้าตัว ตามด้วย newline และตัวปิด ถ้าต้องการชื่อ "Cable" สามารถเปลี่ยนตำแหน่ง newline ให้เป็น '\0' แต่ถ้าข้อมูลเป็น "Cable" เพราะจบข้อมูลก่อน newline การลบสมาชิกสุดท้ายโดยไม่ตรวจจะทำให้เหลือ "Cabl"
จึงไม่ใช้สูตร text[strlen(text) - 1] = '\0'; โดยไม่มีเงื่อนไข เพราะข้อความว่างทำให้ดัชนีไม่ถูกต้อง และข้อความที่ไม่มี newline จะเสียข้อมูลจริง วิธีที่ถูกต้องเริ่มจากค้นว่ามี newline อยู่หรือไม่ แล้วตัดเฉพาะตำแหน่งที่พบ
| เนื้อหาที่อ่านได้ | การทำที่ต้องการ | สิ่งที่ต้องระวัง |
|---|---|---|
"Cable\n" | ตัด newline | เก็บตัว e ไว้ |
"\n" | กลายเป็นสตริงว่าง | ต้องมีกติกาว่าชื่อว่างได้หรือไม่ |
"Cable" แล้วจบข้อมูล | คงข้อความ | อย่าลบตัวท้าย |
| บัฟเฟอร์เต็มไม่มี newline | ตรวจว่ายาวเกินหรือไม่ | อย่าเงียบแล้วรับชื่อที่ถูกตัด |
อีกตัวอย่างคือการต่อรหัสกับคำอธิบาย เช่น "0017" ตามด้วย "-kit" ผลยาวแปดไบต์ใน ASCII ต้องมีพื้นที่อย่างน้อยเก้าไบต์รวมตัวปิด การตรวจความจุควรคิดความยาวข้อความปัจจุบัน บวกข้อความใหม่ บวกหนึ่ง ไม่ใช่ตรวจว่าข้อความใหม่สั้นกว่าปลายทางเพียงอย่างเดียว
ให้วาดบัฟเฟอร์ก่อนและหลังการต่อด้วยมือ แล้วเขียนข้อกำหนดเมื่อพื้นที่ไม่พอว่าจะปฏิเสธหรือแสดงข้อความที่ตัดพร้อมแจ้ง ไม่ใช้ตัวอย่างที่ปล่อยให้เขียนเกินแล้วอธิบายว่าทำงานได้บนเครื่องหนึ่ง การทดสอบความปลอดภัยควรตรวจเส้นทางปฏิเสธก่อนเปลี่ยนข้อมูล
เมื่อทดลองกับภาษาไทย ให้รายงานจำนวนไบต์ที่เครื่องมือเก็บจริงและแยกจากจำนวนหน่วยตัวอักษรที่ผู้อ่านเห็น ไม่ใช้ผล ASCII มาสรุปกับทุกภาษา งานนี้ช่วยให้รู้ขอบเขตของแบบจำลอง char ก่อนนำสตริงไปเป็นชื่อในระเบียน
ใบงานหน่วยความยาว: ไบต์กับสิ่งที่ผู้อ่านเห็น
เริ่มด้วย ASCII เช่น Cable kit แล้วลองข้อความไทยในสภาพแวดล้อมที่บันทึกและส่งเข้าเป็น UTF-8 เปรียบเทียบจำนวนไบต์จาก strlen กับสิ่งที่มองเห็น โดยไม่คาดว่าหนึ่ง char จะเท่ากับหนึ่งตัวอักษรไทยเสมอ
| ข้อความและ encoding ที่ใช้ | ไบต์จาก strlen | ความจุบัฟเฟอร์ | สรุปได้หรือไม่ได้อะไร |
|---|---|---|---|
| ASCII Cable kit | |||
| ข้อความไทยที่ผู้เรียนเลือก ไม่ใช้ชื่อจริง |
ใช้บัฟเฟอร์ที่พอและไม่กลับลำดับ char ทีละไบต์เพื่ออ้างว่ากลับคำภาษาไทยแล้ว บทนี้ไม่สร้างตัวนับ grapheme หรือโปรแกรมตัดคำ การวัดไบต์ยังมีประโยชน์สำหรับคิดความจุ แม้ไม่ใช่ความยาวภาษาธรรมชาติ
8.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: ข้อความสตริงไม่มีเครื่องหมายปิด
ประเภท: ข้อผิดพลาดขณะคอมไพล์
string literal เริ่มด้วยเครื่องหมาย double quote แต่ไม่มีตัวปิดท้าย semicolon ที่เห็นจึงถูกอ่านเป็นส่วนหนึ่งของข้อความที่ยังไม่จบ ข้อความผิดถัดมาอาจเป็นผลตามจากจุดนี้
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int main(void)
{
char code[] = "KIT-01;
puts(code);
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:4:19: warning: missing terminating " character
4 | char code[] = "KIT-01;
| ^
bad.c:4:19: error: missing terminating " character
4 | char code[] = "KIT-01;
| ^~~~~~~~
bad.c:5:5: error: invalid initializer
5 | puts(code);
| ^~~~สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
missing terminating " character หมายถึงขาดเครื่องหมายปิด string literal ไม่ใช่ขาดอักขระ null ในอาร์เรย์ สองเรื่องนี้เกิดคนละขั้น ต้องแก้เครื่องหมาย quote ที่โค้ดก่อนแล้วจึงคอมไพล์ใหม่เพื่อประเมิน error ที่เหลือ
โค้ดที่แก้แล้ว
#include <stdio.h>
int main(void)
{
char code[] = "KIT-01";
puts(code);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
ปิด quote ก่อน semicolon ตามตัวอย่าง อาร์เรย์ที่กำหนดขนาดจาก initializer ที่ถูกต้องจะมีที่สำหรับ terminator ตามข้อความนี้ ไม่ต้องเพิ่มการเขียนนอกอาร์เรย์ด้วยตนเอง
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
KIT-01ตัวอย่างที่ผิด 2: รับทั้งบรรทัดแล้วลืม newline ก่อนเปรียบเทียบ
ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน
input ในตัวอย่างเป็น KIT ตามด้วย newline ซึ่งพอดีกับ buffer fgets เก็บ newline ที่อ่านได้ไว้ด้วย จึงเปรียบเทียบ KIT ตามด้วย newline กับ KIT แล้วไม่เท่ากัน แม้บนหน้าจอดูเหมือนพิมพ์คำเดียวกัน
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
#include <string.h>
int main(void)
{
char code[16];
if (fgets(code, sizeof code, stdin) == NULL) {
return 1;
}
puts(strcmp(code, "KIT") == 0 ? "MATCH" : "NOT MATCH");
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 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ข้อมูลรับเข้า: KIT ตามด้วย newline
ผลจากการรันฉบับผิด
stdout:
NOT MATCHstderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน
สถานะจบที่ตัวเก็บผลอ่านได้: 0
ผลตามข้อกำหนดที่ใช้เทียบ:
MATCHอ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
ไม่มี compiler error และ strcmp ไม่ได้ทำงานผิด สาเหตุอยู่ที่ข้อมูลรับเข้า ใช้การไล่อักขระและความยาวเพื่อแยก newline จาก terminator ไม่ตัดอักขระตัวสุดท้ายทุกครั้งโดยไม่ตรวจ เพราะกรณี input ไม่มี newline อาจตัดข้อมูลจริง
โค้ดที่แก้แล้ว
#include <stdio.h>
#include <string.h>
int main(void)
{
char code[16];
if (fgets(code, sizeof code, stdin) == NULL) {
return 1;
}
code[strcspn(code, "\n")] = '\0';
puts(strcmp(code, "KIT") == 0 ? "MATCH" : "NOT MATCH");
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
ใช้ strcspn หาตำแหน่ง newline แล้วเขียน terminator ในตำแหน่งนั้น ถ้าไม่มี newline จะได้ตำแหน่ง terminator เดิม ตัวอย่างนี้ใช้ input สั้นเฉพาะที่กำหนด กรณีบรรทัดยาวเกิน buffer ยังต้องออกแบบการตรวจและอ่านส่วนค้างแยกต่างหาก
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
MATCHลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 8 การจัดการสตริง
ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกใช้ฟังก์ชันจัดการสตริงใน string.h และการวนอักขระในสตริงด้วยลูป ส่วนที่สองฝึกรับข้อความทั้งบรรทัดที่มีช่องว่างด้วย fgets แล้วแสดงผลพร้อมความยาว ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นการจัดการสตริงตั้งแต่การใช้ฟังก์ชันมาตรฐานไปจนถึงการรับข้อความจากผู้ใช้
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0
Lab A: ฟังก์ชันสตริงและการวนอักขระ
สถานการณ์ ต้องการจัดการชื่อที่เก็บในสตริง โดยหาความยาว คัดลอก เปรียบเทียบ และต่อสตริงด้วยฟังก์ชันใน string.h แล้ววนแสดงอักขระทีละตัวจนพบอักขระว่าง เพื่อฝึกการใช้ฟังก์ชันมาตรฐานและการเข้าถึงอักขระในสตริง
เป้าหมาย ฝึก LLO8.1 และ LLO8.2 คือเข้าถึง/วนอักขระในสตริงจนพบอักขระว่าง และเรียกใช้ฟังก์ชัน strlen strcpy strcmp strcat ได้ถูกต้อง
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch8_string.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch8_string.c : สตริง ความยาว การคัดลอก การเปรียบเทียบ และการต่อสตริง */
#include <stdio.h>
#include <string.h>
int main(void) {
char name[20] = "Somchai";
char copy[20];
printf("name = %s, ความยาว = %zu\n", name, strlen(name));
strcpy(copy, name);
printf("copy = %s\n", copy);
if (strcmp(name, copy) == 0) {
printf("name กับ copy เหมือนกัน\n");
}
char greeting[40] = "Hello, ";
strcat(greeting, name);
printf("greeting = %s\n", greeting);
printf("ตัวอักษร: ");
for (int i = 0; name[i] != '\0'; i++) {
printf("%c-", name[i]);
}
printf("\n");
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch8_string.c -o ch8_string
./ch8_stringผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
name = Somchai, ความยาว = 7
copy = Somchai
name กับ copy เหมือนกัน
greeting = Hello, Somchai
ตัวอักษร: S-o-m-c-h-a-i-strlen(name) คืนความยาว 7 ซึ่งไม่รวมอักขระว่างและพิมพ์ด้วย %zu เพราะเป็นชนิด size_t · strcpy คัดลอก name ไปยัง copy จน strcmp คืน 0 แสดงว่าเท่ากัน · strcat ต่อ name เข้าท้าย greeting ได้ Hello, Somchai · ลูปสุดท้ายวนอักขระด้วยเงื่อนไข name[i] != '\0' จึงแสดงอักขระครบทั้งเจ็ดตัวแล้วหยุดที่อักขระว่าง
ตัดสินใจเอง A.1 ต่อยอดโปรแกรมโดยเขียนลูปนับจำนวนอักขระของ name ด้วยตนเองโดยไม่ใช้ strlen คือวนเพิ่มตัวนับทีละหนึ่งจนพบอักขระว่าง แล้วแสดงค่าที่นับได้เทียบกับผลของ strlen พร้อมตั้งชื่อตัวแปรให้สื่อความหมาย
ผลลัพธ์ที่คาดหวัง A.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และค่าที่นับเองตรงกับ strlen คือ 7 (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ A วนอักขระจนพบอักขระว่างและแสดงครบทุกตัว ความยาวที่นับเองตรงกับ strlen เรียก strlen strcpy strcmp strcat ถูกต้องและผลตรง ใช้ %zu กับ strlen มี #include <string.h> และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| คอมไพล์ไม่ผ่านเพราะไม่รู้จักฟังก์ชันสตริง | ลืม #include <string.h> | เพิ่ม #include <string.h> ที่ต้นไฟล์ |
คอมไพเลอร์แจ้งเตือนชนิดข้อมูลไม่ตรงกับ strlen | ใช้ %d แทน %zu กับค่าที่ strlen คืน | ใช้ %zu ซึ่งตรงกับชนิด size_t |
| โปรแกรมพังตอนต่อสตริง | ปลายทางของ strcpy หรือ strcat เล็กเกินไป | ประกาศปลายทางให้ใหญ่พอรวมอักขระว่างปิดท้าย |
เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว
ใบงานตรวจ Lab A: ตรวจต้นทาง ปลายทาง และตัวปิด
| การทำงาน | สิ่งที่ต้องรู้ก่อนเรียก | กรณีที่ควรทดสอบ |
|---|---|---|
| strlen | มีตัวปิดในพื้นที่ที่อ่านได้ | ว่าง หนึ่งไบต์ และหลายไบต์ |
| strcpy | ปลายทางพอรวมตัวปิดและไม่ทับซ้อนต้นทาง | ชื่อเดิมและชื่อสั้น |
| strcmp | ทั้งสองข้างเป็นสตริงสมบูรณ์ | เท่ากัน ต่างกัน และมีศูนย์นำหน้า |
| strcat | พื้นที่พอสำหรับเนื้อหาเก่า+ใหม่+ตัวปิด | พอดีและขาดหนึ่งช่อง |
งาน A.1 ให้นับจำนวนไบต์ก่อนตัวปิดด้วยลูป แล้วเทียบกับ strlen ใช้ size_t กับตัวนับและ %zu เมื่อรายงานขนาด สำหรับชื่อ Somchai ใน ASCII ได้เจ็ดไบต์ ไม่รวมตัวปิด
โปรแกรม Lab A มี copy[20] และ greeting[40] พอสำหรับค่าคงที่เดิม แต่การเปลี่ยน name ให้ยาวขึ้นต้องตรวจปลายทางทั้งสองใหม่ อย่าใช้ข้อสรุปจากค่าคงที่ไปยืนยันว่าขนาดเดิมพอสำหรับทุกชื่อ
Lab B: อ่านทั้งบรรทัดด้วย fgets
สถานการณ์ ต้องการรับชื่อเต็มที่มีช่องว่างระหว่างชื่อและนามสกุล แล้วแสดงผลพร้อมความยาวของข้อความ เพื่อฝึกการรับสตริงทั้งบรรทัดด้วย fgets และการตัดอักขระขึ้นบรรทัดใหม่
เป้าหมาย ฝึก LLO8.3 คือเขียนโปรแกรมรับและประมวลผลสตริงได้ถูกต้องตามไวยากรณ์ รวมถึงการอ่านทั้งบรรทัดด้วย fgets
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch8_fgets.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch8_fgets.c : อ่านข้อความทั้งบรรทัดด้วย fgets */
#include <stdio.h>
#include <string.h>
int main(void) {
char line[50];
printf("ป้อนชื่อเต็ม: ");
if (fgets(line, sizeof(line), stdin) == NULL) {
puts("Input error");
return 1;
}
size_t end = strcspn(line, "\n");
if (line[end] == '\n') {
line[end] = '\0';
} else if (strlen(line) == sizeof line - 1) {
puts("Line may be too long; not accepted");
return 1;
}
printf("สวัสดี %s (ยาว %zu ไบต์)\n", line, strlen(line));
return 0;
}- คอมไพล์และรันด้วยคำสั่ง แล้วป้อนข้อความ
Somchai Jaidee
gcc -std=c11 -Wall ch8_fgets.c -o ch8_fgets
./ch8_fgetsผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0 โดยป้อน Somchai Jaidee)
ป้อนชื่อเต็ม: สวัสดี Somchai Jaidee (ยาว 14 ไบต์)fgets(line, sizeof(line), stdin) อ่านทั้งบรรทัดรวมช่องว่างระหว่างชื่อและนามสกุลเข้ามาเก็บใน line โดยไม่เกินขนาดบัฟเฟอร์ จากนั้น line[strcspn(line, "\n")] = '\0'; ตัดอักขระขึ้นบรรทัดใหม่ที่ fgets อ่านมาด้วยออก แล้ว strlen(line) คืนความยาว 14 ซึ่งนับรวมช่องว่างหนึ่งช่องระหว่างคำ หากใช้ scanf("%s", ...) แทนจะได้เพียง Somchai เท่านั้น
ตัดสินใจเอง B.1 ต่อยอดโปรแกรมโดยเพิ่มการนับจำนวนช่องว่างในบรรทัดที่รับเข้ามา โดยวนอักขระด้วยลูปจนพบอักขระว่าง และเพิ่มตัวนับทุกครั้งที่พบอักขระช่องว่าง ' ' แล้วแสดงจำนวนช่องว่างที่นับได้ พร้อมตั้งชื่อตัวแปรให้สื่อความหมาย
ผลลัพธ์ที่คาดหวัง B.1 โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือน และนับจำนวนช่องว่างได้ถูกต้อง เช่น จากข้อความ Somchai Jaidee ควรได้ช่องว่างหนึ่งช่อง (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ B fgets อ่านทั้งบรรทัดรวมช่องว่างได้ถูกต้อง ตัดอักขระขึ้นบรรทัดใหม่ด้วย strcspn บัฟเฟอร์ใหญ่พอ แสดงผลและความยาวถูกต้อง คอมไพล์ผ่านไม่มีการแจ้งเตือน และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| ข้อความที่แสดงมีการขึ้นบรรทัดเกินมา | ลืมตัดอักขระขึ้นบรรทัดใหม่ '\n' ที่ fgets อ่านมาด้วย | ใส่ line[strcspn(line, "\n")] = '\0'; ก่อนแสดงผล |
| รับชื่อได้ไม่ครบ ขาดนามสกุล | ใช้ scanf("%s", ...) ที่อ่านถึงช่องว่างแรก | ใช้ fgets อ่านทั้งบรรทัดแทน |
| ข้อความยาวถูกอ่านเพียงส่วนต้น | พื้นที่ไม่พอเก็บทั้งบรรทัด แม้ fgets จะจำกัดการเขียนตามขนาด | ส่งขนาดจริงและตรวจว่าบรรทัดครบหรือปฏิเสธตามนโยบาย |
เปิดหลังผู้เรียนส่งงานแล้ว
ใบงานตรวจ Lab B: บรรทัดว่าง บรรทัดสุดท้าย และขอบเขต
ฉบับ Lab B นี้ใช้บัฟเฟอร์ 50 ไบต์ และปฏิเสธแบบระมัดระวังเมื่อเต็ม 49 ไบต์โดยไม่มี newline อยู่ในบัฟเฟอร์ จึงอาจปฏิเสธเนื้อหาพอดี 49 ไบต์ด้วย ต่างจาก read_two_lines ที่อ่านล่วงหน้าเพื่อแยกกรณี
| ข้อมูล | จุดที่ต้องตรวจ |
|---|---|
| Somchai Jaidee แล้ว newline | ช่องว่างในชื่อยังอยู่ |
| บรรทัดว่าง | อ่านสำเร็จและยาวศูนย์ |
| Cable แล้ว EOF ไม่มี newline | ไม่ลบตัว e |
| เนื้อหา 48 ไบต์แล้ว newline | รับได้และตัดเฉพาะ newline |
| เนื้อหา 49 หรือ 50 ไบต์ | ปฏิเสธตามกติกา ไม่ใช้ส่วนต้นเป็นชื่อ |
| EOF ทันที | ไม่แสดงบัฟเฟอร์ที่อ่านไม่สำเร็จ |
งาน B.1 ให้นับเฉพาะช่องว่าง ' ' ไม่รวม tab หรือ newline โดยอัตโนมัติ ลองข้อความมีช่องว่างต้นบรรทัด ท้ายบรรทัด และสองช่องติดกัน อย่าเรียกจำนวนช่องว่างว่า “จำนวนคำ” โดยไม่มีนิยามเพิ่ม
เก็บกวาด
เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น ch8_string และ ch8_fgets) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด
ตรวจความเข้าใจก่อนเริ่มบทที่ 9
ตอบด้วยผังช่องเก็บหรือสิ่งที่เหลือในสตรีม ไม่ใช้เพียงการท่องชื่อฟังก์ชัน
- ความจุ char array กับ strlen ต่างกันอย่างไร และตัวปิดนับอยู่ฝั่งใด
- เหตุใด
'0'จึงไม่ทำหน้าที่เหมือน'\0' - fgets รับช่องว่างได้ แต่รับประกันว่าจะอ่านครบทั้งบรรทัดทุกครั้งหรือไม่
- ทำไมการลบตัวสุดท้ายโดยไม่ตรวจ newline จึงทำข้อมูลหาย
- เมื่อต่อข้อความ ต้องเผื่อพื้นที่จากความยาวใดบ้าง
- เมื่อ strcmp คืนค่าที่ไม่เป็นศูนย์ ควรตรวจเครื่องหมายอย่างไร
- ข้อความไทยที่ strlen ได้ 12 ไบต์บอกจำนวนตัวอักษรที่ผู้อ่านเห็นแน่นอนหรือไม่
เมื่ออธิบายได้ ให้นำสัญญาของรหัสกับชื่ออุปกรณ์ไปคิดเป็นเขตข้อมูลคนละส่วน แล้วต่อที่ บทที่ 9 โครงสร้างข้อมูลแบบระเบียน
สรุป
บทนี้นำแนวคิดอาร์เรย์จากบทที่ 7 มาใช้กับอาร์เรย์ของอักขระเพื่อเก็บข้อความเป็นสตริง เริ่มจากสตริงคืออาร์เรย์ของอักขระที่ปิดท้ายด้วยอักขระว่าง '\0' เสมอ ประกาศด้วย char name[20] = "Somchai"; แสดงด้วย %s และเข้าถึงอักขระด้วยดัชนีเหมือนอาร์เรย์ โดยข้อความ 7 อักขระใช้พื้นที่ 8 ช่องเพราะต้องเผื่ออักขระว่าง ต่อด้วยฟังก์ชันใน string.h ได้แก่ strlen (คืน size_t พิมพ์ด้วย %zu) strcpy strcmp (คืน 0 เมื่อเท่ากัน) และ strcat ซึ่งต้อง #include <string.h> และต้องมั่นใจว่าปลายทางใหญ่พอ จากนั้นอธิบายการรับสตริง โดย scanf("%s", ...) อ่านถึงช่องว่างแรก ส่วน fgets(buf, ขนาด, stdin) อ่านทั้งบรรทัดและตัดอักขระขึ้นบรรทัดใหม่ด้วย strcspn รวมถึงการวนอักขระจนพบอักขระว่าง และปิดท้ายด้วยข้อควรระวังเรื่องขนาดบัฟเฟอร์ ทักษะเหล่านี้เป็นพื้นฐานของการเก็บและประมวลผลข้อความในบทถัดไป
สิ่งที่ส่งต่อไปยังบทถัดไป
| สิ่งที่ส่งต่อ | ค่า/รูปแบบ | ใช้ในบท |
|---|---|---|
| สตริงคืออาร์เรย์อักขระปิดท้ายด้วยอักขระว่าง | char s[n];, '\0', ขนาด = จำนวนอักขระ + 1 | บท 9 (ชื่อใน struct) และบทที่ประมวลผลข้อความ |
ฟังก์ชันใน string.h และตัวระบุรูปแบบ | strlen/strcpy/strcmp/strcat, %s, %zu | บท 9 เป็นต้นไป |
| การรับทั้งบรรทัดและการวนอักขระ | fgets(buf, ขนาด, stdin), strcspn, for (i=0; s[i] != '\0'; i++) | ทุกบทที่รับข้อความจากผู้ใช้ |
สรุปคำสั่ง
| คำสั่ง | หน้าที่ |
|---|---|
gcc -std=c11 -Wall ch8_string.c -o ch8_string | คอมไพล์ซอร์สโค้ด ch8_string.c เป็นไฟล์ปฏิบัติการ ch8_string พร้อมเปิดการแจ้งเตือน |
gcc -std=c11 -Wall ch8_fgets.c -o ch8_fgets | คอมไพล์ซอร์สโค้ด ch8_fgets.c เป็นไฟล์ปฏิบัติการ ch8_fgets พร้อมเปิดการแจ้งเตือน |
./ch8_string หรือ ./ch8_fgets | รันไฟล์ปฏิบัติการที่คอมไพล์แล้ว |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
กลุ่ม ก (เข้าใจ)
- ก1 (LLO8.1) จงอธิบายว่าสตริง
"Hello"ใช้พื้นที่กี่ช่องในอาร์เรย์ char และเหตุใดจึงต้องมีอักขระว่าง'\0'ปิดท้าย - ก2 (LLO8.2) จงอธิบายว่า
strcmpคืนค่าใดเมื่อสองสตริงเท่ากัน และเหตุใดการใช้ฟังก์ชันในstring.hจึงต้องเพิ่ม#include <string.h>
กลุ่ม ข (ประยุกต์)
- ข1 (LLO8.2) จงเขียนโปรแกรมภาษา C ที่ประกาศสตริงชื่อหนึ่ง แล้วใช้
strlenหาความยาว ใช้strcpyคัดลอกไปยังสตริงอื่น และใช้strcatต่อคำทักทายเข้าท้าย แล้วแสดงผลทั้งหมด โดยพิมพ์ความยาวด้วย%zuแล้วคอมไพล์และรันให้ได้ผลจริง - ข2 (LLO8.3) จงเขียนโปรแกรมที่รับข้อความหนึ่งบรรทัดที่อาจมีช่องว่างด้วย
fgetsตัดอักขระขึ้นบรรทัดใหม่ด้วยstrcspnแล้วแสดงข้อความพร้อมความยาว
กลุ่ม ค (วิเคราะห์)
- ค1 (LLO8.3) จากโปรแกรมที่รับชื่อและนามสกุลด้วย
scanf("%s", ...)จงวิเคราะห์ว่าเหตุใดจึงได้ข้อความไม่ครบ และเสนอวิธีแก้ให้รับได้ทั้งบรรทัด
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาสตริงและอักขระว่างเพิ่มเติมจาก Kernighan และ Ritchie (1988) บทที่ 1 และบทที่ 5 ว่าด้วยอักขระและสตริง แล้วทดลองเขียนโปรแกรมที่วนอักขระเพื่อแปลงตัวพิมพ์เล็กเป็นตัวพิมพ์ใหญ่
- ทบทวนข้อกำหนดเรื่องสตริงและฟังก์ชันใน
string.hในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจว่าเหตุใดภาษา C จึงใช้อักขระว่างปิดท้ายสตริงแทนการเก็บความยาวไว้ต่างหาก - ทดลองออกแบบโปรแกรมที่ใช้
fgetsรับข้อความหลายบรรทัด แล้วนับจำนวนคำในแต่ละบรรทัดด้วยการวนอักขระและตรวจช่องว่าง
เอกสารอ้างอิง
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 ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว
แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 8
- The Open Group: fgets — ขีดจำกัดการอ่าน newline และค่าคืน
- The Open Group: strlen — ความยาวเป็นไบต์ก่อนตัวปิด
- The Open Group: strcmp — ความหมายของเครื่องหมายค่าคืน
- The Open Group: strcat — การต่อรวมตัวปิดและข้อกำหนดพื้นที่ไม่ทับซ้อน
ตรวจเอกสารส่วนที่สอดคล้องกับ ISO C ก่อนเรียบเรียงเมื่อ 23 ตุลาคม 2569 ตัวอย่างใช้ hosted C11 ไม่ใช้ส่วนขยาย POSIX เช่น getline ชุดอ่านบรรทัดใช้ข้อความไม่มี null byte แฝงและทดสอบด้วย LF ไม่ใช่ตัวอ่านข้อมูลไบนารีหรือระบบตรวจ Unicode ทั่วไป