Sign in

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

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

บทที่ 8 การจัดการสตริง

ShareFacebookXLinkedInLINE

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

วิธีสอน

  1. ทบทวนอาร์เรย์หนึ่งมิติและการเข้าถึงสมาชิกด้วยดัชนีจากบทที่ 7 แล้วตั้งคำถามว่าจะเก็บข้อความเช่นชื่อคนไว้ในตัวแปรอย่างไร เพื่อนำเข้าสู่แนวคิดว่าสตริงคืออาร์เรย์ของอักขระ
  2. อธิบายอักขระว่าง '\0' ที่ปิดท้ายสตริง และสาธิตการประกาศ char name[20] = "Somchai"; การแสดงผลด้วย %s และการเข้าถึงอักขระด้วย name[i] บนเครื่องจริง พร้อมย้ำว่าขนาดบัฟเฟอร์ต้องเผื่ออักขระว่างหนึ่งช่องเสมอ
  3. สาธิตการวนอักขระด้วยลูป for จนพบอักขระว่าง และเทียบความยาวที่นับเองกับค่าที่ได้จาก strlen
  4. แนะนำฟังก์ชันใน string.h ได้แก่ strlen strcpy strcmp และ strcat โดยย้ำว่าต้อง #include <string.h> และ strlen คืนค่าชนิด size_t ที่พิมพ์ด้วย %zu
  5. อธิบายการรับสตริงด้วย 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.1CLO1
2ใช้ strlen strcpy strcmp และ strcat จัดการสตริงตามโจทย์ไฟล์ซอร์สโค้ด + ผลการรันLLO8.2CLO1
3รับชื่อเต็มที่มีช่องว่างด้วย fgets ตัดอักขระขึ้นบรรทัดใหม่ แล้วแสดงผลและความยาวไฟล์ซอร์สโค้ด + ผลการรันLLO8.3CLO3

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

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

โค้ดตัวอย่างในบทนี้ทดสอบด้วย 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'

C
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 เพื่อเทียบขนาดอาร์เรย์กับความยาวสตริง ทุกอาร์เรย์ในตัวอย่างตั้งค่าไว้ครบส่วนที่นำมาอ่าน และมีตัวปิดในพื้นที่จริง

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(ปลายทาง, ต้นทาง) ต่อสตริงต้นทางเข้าท้ายปลายทาง

C
#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';

C
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

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

C
/* 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 เพื่อเห็นว่าตัวเลขที่ส่งให้ฟังก์ชันต้องไม่เกินพื้นที่จริง

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

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: ข้อความสตริงไม่มีเครื่องหมายปิด

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

string literal เริ่มด้วยเครื่องหมาย double quote แต่ไม่มีตัวปิดท้าย semicolon ที่เห็นจึงถูกอ่านเป็นส่วนหนึ่งของข้อความที่ยังไม่จบ ข้อความผิดถัดมาอาจเป็นผลตามจากจุดนี้

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

C
#include <stdio.h>
int main(void)
{
    char code[] = "KIT-01;
    puts(code);
    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: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 ที่เหลือ

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

C
#include <stdio.h>
int main(void)
{
    char code[] = "KIT-01";
    puts(code);
    return 0;
}

เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ

ปิด quote ก่อน semicolon ตามตัวอย่าง อาร์เรย์ที่กำหนดขนาดจาก initializer ที่ถูกต้องจะมีที่สำหรับ terminator ตามข้อความนี้ ไม่ต้องเพิ่มการเขียนนอกอาร์เรย์ด้วยตนเอง

ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง

TEXT
KIT-01

ตัวอย่างที่ผิด 2: รับทั้งบรรทัดแล้วลืม newline ก่อนเปรียบเทียบ

ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน

input ในตัวอย่างเป็น KIT ตามด้วย newline ซึ่งพอดีกับ buffer fgets เก็บ newline ที่อ่านได้ไว้ด้วย จึงเปรียบเทียบ KIT ตามด้วย newline กับ KIT แล้วไม่เท่ากัน แม้บนหน้าจอดูเหมือนพิมพ์คำเดียวกัน

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

C
#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;
}

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

TEXT
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:

TEXT
NOT MATCH

stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน

สถานะจบที่ตัวเก็บผลอ่านได้: 0

ผลตามข้อกำหนดที่ใช้เทียบ:

TEXT
MATCH

อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร

ไม่มี compiler error และ strcmp ไม่ได้ทำงานผิด สาเหตุอยู่ที่ข้อมูลรับเข้า ใช้การไล่อักขระและความยาวเพื่อแยก newline จาก terminator ไม่ตัดอักขระตัวสุดท้ายทุกครั้งโดยไม่ตรวจ เพราะกรณี input ไม่มี newline อาจตัดข้อมูลจริง

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

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

TEXT
MATCH

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

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

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

ปฏิบัติการที่ 8 การจัดการสตริง

ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกใช้ฟังก์ชันจัดการสตริงใน string.h และการวนอักขระในสตริงด้วยลูป ส่วนที่สองฝึกรับข้อความทั้งบรรทัดที่มีช่องว่างด้วย fgets แล้วแสดงผลพร้อมความยาว ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นการจัดการสตริงตั้งแต่การใช้ฟังก์ชันมาตรฐานไปจนถึงการรับข้อความจากผู้ใช้

ℹ️Note

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

Lab A: ฟังก์ชันสตริงและการวนอักขระ

สถานการณ์ ต้องการจัดการชื่อที่เก็บในสตริง โดยหาความยาว คัดลอก เปรียบเทียบ และต่อสตริงด้วยฟังก์ชันใน string.h แล้ววนแสดงอักขระทีละตัวจนพบอักขระว่าง เพื่อฝึกการใช้ฟังก์ชันมาตรฐานและการเข้าถึงอักขระในสตริง

เป้าหมาย ฝึก LLO8.1 และ LLO8.2 คือเข้าถึง/วนอักขระในสตริงจนพบอักขระว่าง และเรียกใช้ฟังก์ชัน strlen strcpy strcmp strcat ได้ถูกต้อง

ขั้นปฏิบัติ

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

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

TEXT
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

ขั้นปฏิบัติ

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

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

TEXT
ป้อนชื่อเต็ม: สวัสดี 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

ตอบด้วยผังช่องเก็บหรือสิ่งที่เหลือในสตรีม ไม่ใช้เพียงการท่องชื่อฟังก์ชัน

  1. ความจุ char array กับ strlen ต่างกันอย่างไร และตัวปิดนับอยู่ฝั่งใด
  2. เหตุใด '0' จึงไม่ทำหน้าที่เหมือน '\0'
  3. fgets รับช่องว่างได้ แต่รับประกันว่าจะอ่านครบทั้งบรรทัดทุกครั้งหรือไม่
  4. ทำไมการลบตัวสุดท้ายโดยไม่ตรวจ newline จึงทำข้อมูลหาย
  5. เมื่อต่อข้อความ ต้องเผื่อพื้นที่จากความยาวใดบ้าง
  6. เมื่อ strcmp คืนค่าที่ไม่เป็นศูนย์ ควรตรวจเครื่องหมายอย่างไร
  7. ข้อความไทยที่ 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", ...) จงวิเคราะห์ว่าเหตุใดจึงได้ข้อความไม่ครบ และเสนอวิธีแก้ให้รับได้ทั้งบรรทัด

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

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

By Watcharin Sarachai