6.2 สตริงคืออาเรย์ของ char
ชื่อคน คำศัพท์ ประโยค คือตัวอักษรหลายตัวเรียงกัน ภาษา C ไม่มีชนิดพิเศษสำหรับข้อความ แต่เก็บเป็น อาเรย์ของ char ที่มีตัวปิดท้ายพิเศษหนึ่งตัว ข้อความแบบนี้เรียกว่า สตริง (string) น้องรู้จักอาเรย์จากบทที่ 5 และรู้จัก char จากหน้าที่แล้ว หน้านี้คือเอาสองเรื่องมาต่อกัน
เก็บคำในอาเรย์ของ char
#include <stdio.h>
int main()
{
char name[8] = "Somchai";
for (int i = 0; i < 8; i++) {
printf("name[%d] = %d\n", i, name[i]);
}
return 0;
}
name[0] = 83
name[1] = 111
name[2] = 109
name[3] = 99
name[4] = 104
name[5] = 97
name[6] = 105
name[7] = 0
Somchai มี 7 ตัว แต่ขออาเรย์ 8 ช่อง เพราะเมื่อเขียนข้อความในเครื่องหมายคำพูดคู่ compiler จะเติมช่องสุดท้ายเป็นรหัส 0 ให้เองเสมอ ช่องรหัส 0 นี้เขียนในโค้ดว่า '\0' อ่านว่า null character
ช่องเริ่มที่ 0 เหมือนอาเรย์ทุกตัวในบทที่ 5 ตัวอักษรตัวแรกจึงอยู่ที่ name[0] และตัวสุดท้ายของ Somchai อยู่ที่ name[6]
'\0' บอกว่าข้อความจบตรงไหน
ทำไมต้องมีตัวปิดท้าย เพราะส่วนใหญ่เราไม่รู้ล่วงหน้าว่าข้อความจะยาวเท่าไร จึงขออาเรย์เผื่อไว้ใหญ่ๆ เช่น 20 ช่อง แต่ใส่ชื่อแค่ 7 ตัว โปรแกรมต้องมีวิธีรู้ว่าข้อความจริงจบที่ช่องไหน คำตอบคือหยุดที่ '\0' ตัวแรก printf กับ %s ก็ทำแบบนั้น พิมพ์ทีละช่องไปจนเจอ '\0'
#include <stdio.h>
int main()
{
char name[20] = "Somchai";
printf("%s\n", name);
name[3] = '\0';
printf("%s\n", name);
name[3] = 'c';
printf("%s\n", name);
return 0;
}
Somchai
Som
Somchai
บรรทัดที่ 7 ใส่ '\0' ทับตัว c ข้อความจึงเหลือแค่ Som แต่ตัว h a i ยังอยู่ในช่อง 4 ถึง 6 เหมือนเดิม แค่ printf หยุดก่อนถึง พอใส่ c กลับคืนในบรรทัดที่ 9 ชื่อเต็มก็กลับมา ส่วนช่อง 7 ถึง 19 ที่ไม่ได้ใช้ compiler เติม 0 ไว้ให้ทั้งหมด เพราะประกาศพร้อมกำหนดค่า
'\0' ไม่ใช่ '0'
'\0' คือรหัส 0 ใช้ปิดท้ายข้อความ ส่วน '0' คือตัวอักษรเลขศูนย์ รหัส 48 เครื่องหมาย \ ทำงานแบบเดียวกับใน '\n' คือบอกว่าตัวถัดไปมีความหมายพิเศษ
กฎที่ต้องจำจากเรื่องนี้ อาเรย์ต้องมีขนาดอย่างน้อยความยาวข้อความบวกหนึ่ง โจทย์บอกว่าคำยาวไม่เกิน 100 ตัว ต้องประกาศอย่างน้อย char word[101] คนส่วนใหญ่เผื่อเป็น 105 ไว้กันนับพลาด
รับคำด้วย %s
บทที่ 2 ใช้ %s รับคำมาแล้ว ทบทวนสั้นๆ scanf("%s", word) ข้ามช่องว่างข้างหน้า อ่านตัวอักษรไปจนเจอช่องว่างหรือขึ้นบรรทัดใหม่ แล้วเติม '\0' ปิดท้ายให้เอง ไม่ต้องมี & หน้าชื่ออาเรย์ เพราะชื่ออาเรย์บอกที่อยู่ในหน่วยความจำอยู่แล้ว เหตุผลเต็มๆ อยู่ในบทที่ 10
พอได้คำมาแล้ว งานเกือบทุกอย่างคือวนลูปทีละช่องตั้งแต่ช่อง 0 จนกว่าจะเจอ '\0' ต่างจากอาเรย์ตัวเลขในบทที่ 5 ที่ต้องเก็บ N ไว้ในตัวแปรอีกตัวว่าใช้ไปกี่ช่อง สตริงบอกจุดจบได้เอง
#include <stdio.h>
int main()
{
char word[105];
scanf("%s", word);
int length = 0, vowels = 0;
for (int i = 0; word[i] != '\0'; i++) {
length++;
char c = word[i];
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
vowels++;
}
}
printf("length %d vowels %d\n", length, vowels);
return 0;
}
programming
length 11 vowels 3
ลูป for นี้มีเงื่อนไขเป็น word[i] != '\0' แทนที่จะเป็น i < n อ่านว่า "ทำไปเรื่อยๆ ตราบที่ช่องนี้ยังไม่ใช่ตัวปิดท้าย" ความยาวจึงนับได้ไปพร้อมกัน
เพราะ %s หยุดที่ช่องว่าง ถ้าเอาไปใส่ใน while แบบที่เรียนในบทที่ 4 จะได้ข้อความทีละคำจนหมด ไม่ว่าคำจะคั่นด้วยช่องว่างกี่ช่องหรือขึ้นบรรทัดใหม่
#include <stdio.h>
int main()
{
char word[105];
while (scanf("%s", word) == 1) {
int length = 0;
while (word[length] != '\0') {
length++;
}
printf("%s %d\n", word, length);
}
return 0;
}
I love programming
very much
I 1
love 4
programming 11
very 4
much 4
ลูป while ตัวในนับความยาวด้วยการเดินไปจนเจอ '\0' ที่ช่อง word[length] พอดี เพราะตัวสุดท้ายอยู่ที่ช่อง length - 1 งานนับความยาวนี้มีฟังก์ชันสำเร็จรูปชื่อ strlen ซึ่งจะเรียนในหน้า 6.4
สตริงยาวประกาศนอก main
อาเรย์ char ช่องละ 1 byte สตริงยาวหนึ่งล้านตัวจึงใช้ราว 1 MB บนเครื่องที่เขียนเอกสารนี้ ตัวแปรใน main มีที่รวมกันราว 2 MB สตริงหนึ่งล้านตัวในหนึ่ง main ยังรอด แต่ขยายเป็นสามล้านตัวโปรแกรมพังทันทีตอนเริ่มรัน และเครื่อง grader อาจให้ที่น้อยกว่านี้ ไม่ต้องมานั่งคำนวณ ใช้วิธีเดียวกับอาเรย์ใหญ่ในบทที่ 5 คือสตริงที่ยาวหลักแสนขึ้นไปให้ประกาศไว้นอก main เสมอ
#include <stdio.h>
char votes[1000005];
int main()
{
scanf("%s", votes);
int yes = 0, no = 0;
for (int i = 0; votes[i] != '\0'; i++) {
if (votes[i] == 'Y') {
yes++;
} else {
no++;
}
}
printf("yes %d no %d\n", yes, no);
return 0;
}
YNYYNYY
yes 5 no 2
โปรแกรมนับผลโหวตนี้รับได้ถึงหนึ่งล้านเสียงในบรรทัดเดียว ขนาด 1000005 คือหนึ่งล้านตัวบวกช่อง '\0' และเผื่ออีกนิด
ทำไมโจทย์ใช้แต่ตัวอักษรภาษาอังกฤษ
ตาราง ASCII มีแค่ 128 ตัว ไม่มีภาษาไทย ตัวอักษรไทยหนึ่งตัวในรหัส UTF-8 ที่เว็บและไฟล์ส่วนใหญ่ใช้กินที่ 3 byte เก็บใน char ตัวเดียวไม่ได้ ถ้าวนลูปทีละช่อง หนึ่งช่องจะได้แค่ชิ้นส่วนหนึ่งในสามของตัวอักษร นับความยาวหรือกลับคำก็ผิดหมด
โจทย์ TOI Zero และโจทย์ค่ายจึงใช้แค่ตัวอักษรภาษาอังกฤษ ตัวเลข และเครื่องหมายใน ASCII ผลลัพธ์ที่น้องพิมพ์ก็ควรเป็นภาษาอังกฤษด้วย เพราะหน้าต่างสีดำของ Windows แสดงภาษาไทยเพี้ยน ทุกตัวอย่างในเอกสารนี้จึงพิมพ์เป็นภาษาอังกฤษ
ลองทำให้พัง
แบบที่ 1 ไม่เหลือช่องให้ '\0'
#include <stdio.h>
int main()
{
char word[5] = "hello";
int score = 7;
printf("%s\n", word);
printf("%d\n", score);
return 0;
}
คอมไพล์ผ่านด้วย -Wall โดยไม่มีคำเตือน ภาษา C ยอมให้ข้อความยาวเท่าอาเรย์พอดี แล้วไม่ใส่ '\0' ให้ บนเครื่องที่เขียนเอกสารนี้ พิมพ์ hello แล้วมีรหัสแปลกๆ ที่ไม่ได้สั่งต่อท้าย คอมไพล์เพิ่ม -O2 ได้ห้าตัว คอมไพล์ตามที่ตั้งใน Code::Blocks ได้รหัส 7 ตัวเดียวซึ่งเป็นเสียงบี๊บ เพราะ printf หา '\0' ไม่เจอในห้าช่องของ word จึงพิมพ์เลยไปเรื่อยๆ ในหน่วยความจำที่อยู่ถัดไป จนบังเอิญเจอรหัส 0 ที่ไหนสักแห่ง
ถ้าลดเหลือ char word[4] ซึ่งเล็กกว่าข้อความ compiler ถึงจะเตือน
word.c:5:20: warning: initializer-string for array of 'char' is too long
5 | char word[4] = "hello";
| ^~~~~~~
สองแบบนี้แก้เหมือนกัน นับตัวอักษรแล้วบวกหนึ่ง หรือไม่ใส่ขนาดเลย เขียน char word[] = "hello"; แล้วให้ compiler นับให้ ได้ 6 ช่องพอดี
แบบที่ 2 ใส่ & หน้าชื่ออาเรย์ใน scanf
กับดักเดียวกับแบบที่ 2 ในหน้า 2.3 ถ้าติดนิสัยจาก %d แล้วแก้บรรทัดที่ 6 ของโปรแกรม vowels เป็น scanf("%s", &word); จะได้คำเตือน format '%s' expects argument of type 'char *' แบบเดียวกับหน้านั้น ต่างแค่ชนิดที่ขึ้นเป็น char (*)[105] ตามขนาดของ word ตอนนี้รู้เหตุผลแล้วว่าชื่ออาเรย์บอกที่อยู่อยู่แล้ว จึงไม่ต้องมี & ส่วนชนิด char (*)[105] จะเข้าใจในบทที่ 10 บนเครื่องที่เขียนเอกสารนี้โปรแกรมบังเอิญยังทำงานถูก แต่คำเตือนเรื่อง format ทุกอันต้องแก้ ไม่ต้องรอให้พัง
สรุป
- สตริงคืออาเรย์ของ
charที่ปิดท้ายด้วย'\0'ซึ่งคือรหัส 0 - ข้อความในเครื่องหมายคำพูดคู่มี
'\0'ต่อท้ายเสมอ อาเรย์จึงต้องใหญ่กว่าความยาวอย่างน้อยหนึ่งช่อง %sพิมพ์จนเจอ'\0'และรับคำทีละคำ หยุดที่ช่องว่าง ไม่มี&หน้าชื่ออาเรย์- วนลูปสตริงด้วยเงื่อนไข
word[i] != '\0' - สตริงยาวหลักแสนหลักล้านประกาศนอก
main - โจทย์แข่งขันใช้ตัวอักษร ASCII เท่านั้น ผลลัพธ์พิมพ์เป็นภาษาอังกฤษ