10.4 สตริงกับ char *
สตริงในบทที่ 6 คืออาเรย์ของ char ที่ปิดท้ายด้วย '\0' พอรู้ว่าชื่ออาเรย์คือที่อยู่ของช่องแรก ก็จะเห็นว่าฟังก์ชันสตริงทุกตัว ตั้งแต่ printf("%s") จนถึง strlen ทำงานกับ char * คือที่อยู่ของตัวอักษรตัวหนึ่ง แล้วเดินไปเรื่อยๆ จนเจอ '\0' หน้านี้ใช้ความรู้นี้ตัดสตริงเป็นท่อน ลบตัวอักษรออกจากสตริงโดยไม่ใช้อาเรย์ที่สอง และอธิบายคำเตือนกับผลแปลกๆ ของสตริงที่ค้างไว้ตั้งแต่บทที่ 2 และบทที่ 6
%s รับที่อยู่ แล้วพิมพ์จนเจอ \0
printf("%s", word) ไม่ได้รับทั้งอาเรย์ มันได้แค่ที่อยู่ของ word[0] ซึ่งมีชนิด char * แล้วพิมพ์ทีละตัวไปจนเจอ '\0' ถ้าส่งที่อยู่ของช่องอื่นไป มันก็เริ่มพิมพ์จากช่องนั้น
#include <stdio.h>
int main()
{
char word[] = "programming";
printf("%s\n", word);
printf("%s\n", word + 3);
printf("%c\n", *(word + 3));
for (char *p = word + 7; *p != '\0'; p++) {
printf("%s\n", p);
}
return 0;
}
programming
gramming
g
ming
ing
ng
g
- บรรทัดที่ 7
word + 3คือที่อยู่ของword[3]ซึ่งเก็บg%sจึงพิมพ์ตั้งแต่ตัวนั้นไปจนจบ - บรรทัดที่ 8
*(word + 3)คือตัวอักษรในช่องนั้นหนึ่งตัว ชนิดcharจึงพิมพ์ด้วย%cความต่างระหว่างchar *กับcharคือความต่างระหว่าง "ข้อความที่เริ่มตรงนี้" กับ "ตัวอักษรตัวนี้" - ลูปบรรทัดที่ 9 เลื่อน
pทีละตัว ทุกครั้งที่พิมพ์%sจึงได้ส่วนท้ายที่สั้นลงทีละตัว ลูปหยุดเมื่อ*pเป็น'\0'แบบเดียวกับที่printfทำข้างใน
scanf("%s", word) ก็เหมือนกัน มันต้องการที่อยู่ของช่องแรกที่จะเริ่มเขียน ชื่อ word ให้ที่อยู่นั้นอยู่แล้ว จึงไม่ต้องมี & ตามที่หน้า 2.3 บอกไว้
&word คือที่อยู่ของทั้งอาเรย์
หน้า 2.3 และหน้า 6.2 ลองใส่ & หน้าชื่ออาเรย์ใน scanf แล้วได้คำเตือนว่า '%s' expects argument of type 'char *', but argument 2 has type 'char (*)[105]' ทั้งที่โปรแกรมยังทำงานถูก ลองพิมพ์ที่อยู่ดู
#include <stdio.h>
int main()
{
char word[105] = "pointer";
printf("word = %p\n", word);
printf("&word = %p\n", &word);
printf("word + 1 = %p\n", word + 1);
printf("&word + 1 = %p\n", &word + 1);
return 0;
}
word = 00000081D6DFFBC0
&word = 00000081D6DFFBC0
word + 1 = 00000081D6DFFBC1
&word + 1 = 00000081D6DFFC29
word กับ &word เป็นเลขเดียวกัน เพราะอาเรย์เริ่มที่ช่องแรก แต่ชนิดต่างกัน word คือที่อยู่ของ char หนึ่งตัว (char *) ส่วน &word คือที่อยู่ของทั้งอาเรย์ 105 ช่อง ชนิดของมันเขียนว่า char (*)[105] อ่านว่า pointer ไปยังอาเรย์ของ char 105 ช่อง ความต่างโผล่ตอนบวก 1 word + 1 ขยับ 1 byte ไปตัวอักษรถัดไป แต่ &word + 1 ขยับไปทั้งก้อน จาก BC0 เป็น C29 ซึ่งห่างกัน 69 ในฐานสิบหก คือ 105 พอดี
scanf เขียนตัวอักษรทีละตัวเริ่มจากที่อยู่ที่ได้ เลขที่อยู่ตรงกันจึงบังเอิญทำงานถูก แต่ชนิดผิด compiler จึงเตือน และถ้าเอาไปบวกเลขแบบในตัวอย่างข้างบน ความต่างของชนิดจะได้ที่อยู่ผิดไปทันที เขียนชื่ออาเรย์เปล่าๆ ทุกครั้ง
เดินบนสตริงด้วย pointer
ฟังก์ชันที่รับสตริงจึงรับเป็น char * ได้ แล้วเดินด้วย pointer จนเจอ '\0' ไม่ต้องรับความยาวเพราะสตริงบอกจุดจบเอง ตัวอย่างนี้เขียน strlen เอง และนับตัวพิมพ์ใหญ่
#include <stdio.h>
int length(char *s)
{
char *p = s;
while (*p != '\0') {
p++;
}
return (int)(p - s); // (1)!
}
int count_upper(char *s)
{
int count = 0;
for (char *p = s; *p != '\0'; p++) {
if (*p >= 'A' && *p <= 'Z') {
count++;
}
}
return count;
}
int main()
{
char name[105];
scanf("%s", name);
printf("length %d, upper %d\n", length(name), count_upper(name));
return 0;
}
pหยุดที่ช่องของ'\0'ระยะห่างจากช่องแรกคือจำนวนตัวอักษรก่อน'\0'พอดี ใช้การลบ pointer จากหน้า 10.3
SongkhlaNakarin
length 15, upper 2
เทียบกับหน้า 8.3 ที่เขียน char text[] แล้วใช้ text[i] สองแบบทำงานเหมือนกัน แบบ index อ่านง่ายเมื่อต้องรู้ตำแหน่ง แบบ pointer สะดวกเมื่อต้องจำ "ตรงนี้" ไว้ใช้ต่อ อย่างในหัวข้อถัดไป
ฟังก์ชันที่คืนที่อยู่ในสตริง
อีเมลแบ่งเป็นชื่อผู้ใช้กับโดเมน คั่นด้วย @ ฟังก์ชัน find_char คืนที่อยู่ของตัวที่หาเจอตัวแรก หรือคืน NULL ถ้าไม่มี เมื่อได้ที่อยู่ของ @ แล้ว เขียน '\0' ทับตรงนั้น สตริงเดียวก็กลายเป็นสองท่อนทันที
#include <stdio.h>
char *find_char(char *s, char c) // (1)!
{
for (char *p = s; *p != '\0'; p++) {
if (*p == c) {
return p;
}
}
return NULL; // (2)!
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
char email[105];
scanf("%s", email);
char *at = find_char(email, '@');
if (at == NULL) {
printf("%s is not an email\n", email);
} else {
*at = '\0'; // (3)!
printf("user %s, domain %s\n", email, at + 1); // (4)!
}
}
return 0;
}
- ชนิดที่คืนคือ
char *ที่อยู่ที่คืนเป็นของช่องในอาเรย์emailของmainซึ่งยังอยู่หลังฟังก์ชันจบ จึงคืนได้ ต่างจากแบบที่ 4 ของหน้า 10.2 - เดินจนจบแล้วไม่เจอ คืน
NULLตามความหมายในหน้า 10.1 - แก้ช่องที่
atชี้ จาก'@'เป็น'\0'สตริงที่เริ่มจากemailจึงจบตรงนี้ - โดเมนเริ่มที่ตัวถัดจาก
@และจบที่'\0'เดิมท้ายอีเมล
3
ploy@psu.ac.th
somchai.k@posn.or.th
hello
user ploy, domain psu.ac.th
user somchai.k, domain posn.or.th
hello is not an email
ไม่มีการคัดลอกตัวอักษรเลยสักตัว สองท่อนยังอยู่ในอาเรย์เดิม แค่มี pointer สองตัวชี้จุดเริ่มของแต่ละท่อน และมี '\0' ปิดท้ายแต่ละท่อน วิธีนี้ตัดสตริงยาวเป็นท่อนได้เร็วมาก แต่สตริงเดิมจะเสียไป ถ้ายังต้องใช้ทั้งอีเมลอีก ให้พิมพ์หรือคัดลอกไว้ก่อนตัด
pointer อ่านกับเขียนบนสตริงเดียวกัน
เบอร์โทร 081-234-5678 ต้องการเอาขีดออกให้เหลือแต่ตัวเลข วิธีหนึ่งคือคัดลอกตัวที่ไม่ใช่ขีดไปอีกอาเรย์ แต่ทำในอาเรย์เดิมได้ด้วย pointer สองตัว read เดินอ่านทุกตัว write ชี้ช่องถัดไปที่จะเขียน ตัวที่จะเก็บไว้ถูกคัดลอกจาก read ไป write
#include <stdio.h>
void remove_char(char *s, char c)
{
char *write = s;
for (char *read = s; *read != '\0'; read++) {
if (*read != c) {
*write = *read; // (1)!
write++;
}
}
*write = '\0'; // (2)!
}
int main()
{
char phone[105];
scanf("%s", phone);
remove_char(phone, '-');
printf("%s\n", phone);
return 0;
}
writeไม่มีทางแซงreadเพราะขยับได้อย่างมากเท่ากับreadตัวที่เขียนทับจึงเป็นตัวที่อ่านไปแล้วเสมอ ไม่ทำให้ข้อมูลที่ยังไม่ได้อ่านเสีย- สตริงใหม่สั้นกว่าเดิม ต้องปิดท้ายตรงที่
writeหยุด ไม่งั้น'\0'ยังอยู่ที่เดิมท้ายสตริงยาว ดูแบบที่ 2 ในส่วนลองทำให้พัง
081-234-5678
0812345678
เดินรอบเดียว ไม่ต้องเลื่อนตัวอักษรทั้งแถวทุกครั้งที่ลบแบบหน้า 5.5 ถ้าลบด้วยการเลื่อนทั้งแถว สตริงยาว n ที่ต้องลบเกือบทุกตัวจะเลื่อนรวมราว n2 / 2 ครั้ง แบบสองตัวชี้ใช้แค่ n ครั้ง เปลี่ยนเงื่อนไขในบรรทัดที่ 7 ก็ได้งานอื่น เช่น ลบสระ เก็บเฉพาะตัวเลข หรือยุบช่องว่างที่ติดกัน
ข้อความในเครื่องหมายคำพูด
ข้อความอย่าง "Monday" ในโค้ดก็เป็นอาเรย์ของ char ที่ compiler เตรียมไว้ให้ในหน่วยความจำส่วนที่อ่านได้อย่างเดียว เมื่อใช้เป็นค่าจะได้ที่อยู่ของตัวแรก เก็บที่อยู่นี้ไว้ใน const char * ได้ คำว่า const หน้าชนิดคือสัญญาว่าจะไม่แก้ตัวอักษรผ่าน pointer นี้ ถ้าเผลอแก้ compiler จะฟ้อง อาเรย์ของ const char * จึงเป็นตารางข้อความที่สะดวกมาก
#include <stdio.h>
int main()
{
const char *days[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; // (1)!
int today, after;
scanf("%d %d", &today, &after);
printf("%s\n", days[(today + after) % 7]);
return 0;
}
- อาเรย์เจ็ดช่อง แต่ละช่องเก็บ ที่อยู่ ของข้อความหนึ่งข้อความ ไม่ได้เก็บตัวอักษรเอง ข้อความจึงยาวไม่เท่ากันได้ ต่างจากอาเรย์สองมิติของ
charในบทที่ 7 ที่ทุกแถวยาวเท่ากัน
1 10
Thursday
วันนี้วันจันทร์ (วันที่ 1 เมื่อนับวันอาทิตย์เป็น 0) อีก 10 วันสอบ (1 + 10) % 7 = 4 คือวันพฤหัสบดี
ต้องแยกให้ออกระหว่างสองบรรทัดนี้ ซึ่งหน้าตาเกือบเหมือนกัน
| เขียน | ได้อะไร | แก้ตัวอักษรได้ไหม |
|---|---|---|
char name[] = "Mali"; | อาเรย์ 5 ช่องของเราเอง ข้อความถูกคัดลอกมาใส่ | ได้ name[0] = 'B'; ได้ Bali |
const char *name = "Mali"; | pointer ตัวเดียว เก็บที่อยู่ของข้อความในส่วนที่อ่านได้อย่างเดียว | ไม่ได้ ดูแบบที่ 1 ในส่วนลองทำให้พัง |
เรื่องนี้อธิบายผลแปลกของ answer == "yes" ในหน้า 6.4 ได้ด้วย ทั้งสองข้างของ == ถูกแปลงเป็นที่อยู่ == จึงเทียบที่อยู่ ไม่ได้เทียบตัวอักษร ลองพิมพ์ที่อยู่ของทั้งสองดู
#include <stdio.h>
int main()
{
char answer[10] = "yes";
printf("answer is at %p\n", answer);
printf("\"yes\" is at %p\n", "yes");
return 0;
}
answer is at 000000C98C3FF796
"yes" is at 00007FF7F8755061
บนเครื่องที่เขียนเอกสารนี้ อาเรย์ answer อยู่ย่านเดียวกับตัวแปรอื่นใน main ส่วน "yes" อยู่ห่างออกไปมากในส่วนที่อ่านได้อย่างเดียว ตัวอักษรในสองที่นั้นเหมือนกันทุกตัว แต่ที่อยู่ไม่มีทางเท่ากัน == จึงได้เท็จเสมอ เทียบสตริงต้องใช้ strcmp ซึ่งเดินเทียบทีละตัวอักษรด้วย pointer แบบในหน้านี้
ลองทำให้พัง
แบบที่ 1 แก้ข้อความในเครื่องหมายคำพูด
ลืมใส่ const แล้วแก้ตัวอักษรผ่าน pointer
#include <stdio.h>
int main()
{
char *name = "Mali";
name[0] = 'B';
printf("%s\n", name);
return 0;
}
คอมไพล์ด้วย -Wall ไม่มีคำเตือน เพราะไม่มี const ให้ compiler ตรวจ แต่รันแล้วไม่พิมพ์อะไร บนเครื่องที่เขียนเอกสารนี้ได้
Process returned -1073741819 (0xC0000005)
name[0] = 'B' พยายามเขียนลงหน่วยความจำส่วนที่อ่านได้อย่างเดียว Windows จึงปิดโปรแกรม ถ้าเขียน const char *name = "Mali"; ตั้งแต่แรก บรรทัดที่ 6 จะกลายเป็น error assignment of read-only location '*name' ตอนคอมไพล์ ซึ่งดีกว่าพังตอนรันมาก ถ้าต้องการข้อความที่แก้ได้ ให้ใช้อาเรย์ char name[] = "Mali";
แบบที่ 2 ลืมปิดท้ายด้วย \0
ในโปรแกรม phone ลบบรรทัด *write = '\0'; ออก
#include <stdio.h>
void remove_char(char *s, char c)
{
char *write = s;
for (char *read = s; *read != '\0'; read++) {
if (*read != c) {
*write = *read;
write++;
}
}
}
int main()
{
char phone[105];
scanf("%s", phone);
remove_char(phone, '-');
printf("%s\n", phone);
return 0;
}
081-234-5678
081234567878
สิบตัวแรกถูกแล้ว แต่มี 78 เกินมาท้าย สองช่องนั้นคือตัวอักษรเดิมของสตริงยาว ที่ write ไปไม่ถึง และ '\0' ยังอยู่ที่ตำแหน่งเดิมคือช่องที่ 12 printf จึงพิมพ์ไปถึงตรงนั้น เขียนผ่าน pointer เมื่อไร ต้องถามตัวเองว่าสตริงจบที่ไหน
แบบที่ 3 ใช้ผลที่เป็น NULL
ในโปรแกรม email ตัด if (at == NULL) ออก แล้วตัดอีเมลทันที
#include <stdio.h>
char *find_char(char *s, char c)
{
for (char *p = s; *p != '\0'; p++) {
if (*p == c) {
return p;
}
}
return NULL;
}
int main()
{
char email[105];
scanf("%s", email);
char *at = find_char(email, '@');
*at = '\0';
printf("user %s, domain %s\n", email, at + 1);
return 0;
}
ป้อน ploy@psu.ac.th ทำงานถูก แต่ป้อน somchai.school.ac.th ที่พิมพ์ @ ผิดเป็นจุด โปรแกรมไม่พิมพ์อะไรเลย บนเครื่องที่เขียนเอกสารนี้ได้ Process returned -1073741819 (0xC0000005) เพราะ at เป็น NULL แล้วบรรทัดที่ 18 เขียนผ่านมัน แบบเดียวกับแบบที่ 5 ของหน้า 10.1 ฟังก์ชันที่อาจคืน NULL ต้องตรวจค่าที่คืนก่อนใช้ทุกครั้ง ชุดทดสอบของโจทย์มักมีกรณี "ไม่เจอ" ซ่อนอยู่เสมอ
แบบที่ 4 ส่งตัวอักษรตัวเดียวให้ %s
#include <stdio.h>
int main()
{
char word[105] = "pointer";
printf("%s\n", word[0]);
return 0;
}
first.c:6:14: warning: format '%s' expects argument of type 'char *', but argument 2 has type 'int' [-Wformat=]
6 | printf("%s\n", word[0]);
| ~^ ~~~~~~~
| | |
| char * int
| %d
word[0] คือตัว p ซึ่งมีรหัส 112 ไม่ใช่ที่อยู่ (compiler บอกว่าเป็น int เพราะ char ถูกแปลงเป็น int ตอนส่งให้ printf) แต่ %s จะเอา 112 ไปใช้เป็นที่อยู่แล้วตามไปอ่าน บนเครื่องที่เขียนเอกสารนี้โปรแกรมพังด้วย 0xC0000005 ทุกครั้ง อยากพิมพ์ตัวอักษรตัวเดียวใช้ %c อยากพิมพ์ข้อความตั้งแต่ตัวนั้นใช้ %s กับ word + 0 หรือ &word[0]
สรุป
- ชื่ออาเรย์
charที่ใช้เป็นค่าคือchar *ที่อยู่ของตัวแรก%sรับที่อยู่แล้วพิมพ์ไปจนเจอ'\0'ส่งword + kก็ได้ข้อความตั้งแต่ตัวที่k &wordมีเลขที่อยู่เท่ากับwordแต่ชนิดเป็นchar (*)[105]ในscanf("%s")ใช้ชื่ออาเรย์เปล่าๆ- เดินสตริงด้วย
for (char *p = s; *p != '\0'; p++)และความยาวคือp - sตอนเจอ'\0' - ฟังก์ชันคืน
char *ชี้เข้าไปในสตริงได้ และคืนNULLเมื่อไม่เจอ คนเรียกต้องตรวจNULLก่อนใช้ - เขียน
'\0'ทับตรงไหน สตริงก็จบตรงนั้น ใช้ตัดสตริงเป็นท่อน และต้องปิดท้ายทุกครั้งที่เขียนสตริงใหม่ด้วย pointer - ข้อความในเครื่องหมายคำพูดแก้ไม่ได้ เก็บด้วย
const char *และ==กับสตริงเทียบที่อยู่ ไม่ได้เทียบตัวอักษร