C สตริงคืออาร์เรย์ของ char
C ไม่มีชนิด “string” สตริงคือ อาร์เรย์ของ char ที่ลงท้ายด้วยไบต์ค่า 0 ('\0' เรียกว่า null terminator)
"Hello" ในหน่วยความจำ:
┌───┬───┬───┬───┬───┬────┐
│'H'│'e'│'l'│'l'│'o'│'\0'│ ← 6 ไบต์ สำหรับข้อความ 5 ตัว
└───┴───┴───┴───┴───┴────┘ฟังก์ชันทุกตัวที่ทำงานกับสตริง (printf %s, strlen, …) อ่านไปเรื่อย ๆ จนเจอ '\0' ถ้าไม่มี '\0' มันจะอ่านเลยขอบไปเรื่อย ๆ — buffer over-read
สองวิธีสร้างสตริง
#include <stdio.h>
int main(void)
{
char a[] = "Hello"; // อาร์เรย์ char ขนาด 6 (รวม '\0') — แก้ไขได้
char b[20] = "Hi"; // อาร์เรย์ขนาด 20, ใช้ 3 ไบต์แรก, ที่เหลือ '\0'
const char *c = "World"; // pointer ชี้ไป string literal — ห้ามแก้!
a[0] = 'J'; // OK → "Jello"
// c[0] = 'w'; // UB — ห้ามแก้ string literal (มักวางใน read-only memory → crash)
printf("%s %s %s\n", a, b, c);
return 0;
}char s[] = "..." | char *s = "..." | |
|---|---|---|
| เก็บที่ไหน | อาร์เรย์ของเรา (บน stack ถ้าเป็น local, บน static storage ถ้า global/static) | pointer ชี้ไป string literal |
| แก้ค่าได้ไหม | ได้ | ไม่ได้ — แก้แล้วเป็น UB (คอมไพเลอร์มักวาง literal ไว้ในหน่วยความจำ read-only แต่มาตรฐานบอกแค่ว่า “ห้ามแก้”) |
sizeof | ขนาดอาร์เรย์ (รวม '\0') | ขนาด pointer (มัก 8 บนระบบ 64 บิต) |
| นิยมประกาศเป็น | char s[] เมื่อจะแก้ | const char *s เมื่อแค่อ่าน |
strlen — ความยาว (ไม่นับ ‘\0’)
#include <string.h>
#include <stdio.h>
int main(void)
{
char s[] = "Hello";
printf("%zu\n", strlen(s)); // 5 (นับจนก่อน '\0')
printf("%zu\n", sizeof(s)); // 6 (ขนาดอาร์เรย์ รวม '\0')
return 0;
}strlen คืน size_t — ใช้ %zu strlen นับทุกครั้งที่เรียก (O(n)) อย่าเรียกในเงื่อนไข loop:
for (size_t i = 0; i < strlen(s); i++) { ... } // ❌ ช้า — นับใหม่ทุกรอบ
size_t n = strlen(s);
for (size_t i = 0; i < n; i++) { ... } // ✅คัดลอกสตริง
char dest[10];
char src[] = "Hello";
// strcpy — คัดลอกจนเจอ '\0' ของ src
strcpy(dest, src); // dest = "Hello"
// ❌ อันตราย: ถ้า src ยาวกว่า dest จะเขียนทะลุ = buffer overflow
char small[3];
strcpy(small, "Hello World"); // เขียน 12 ไบต์ลงพื้นที่ 3 ไบต์ — เขียนทับตัวแปรอื่น / crashใช้ strncpy หรือ snprintf เพื่อจำกัดขนาด:
char dest[10];
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // strncpy ไม่รับประกันว่าปิด '\0' ถ้า src ยาวเกิน!
// ทางที่ชัดเจนกว่า
snprintf(dest, sizeof(dest), "%s", src); // ตัดพอดี + ปิด '\0' เสมอstrncpy มีพฤติกรรมแปลก: ถ้า src สั้นกว่า n มันเติม '\0' จนเต็ม (เปลืองแรง), ถ้ายาวกว่าหรือเท่า ไม่ปิด '\0' — ต้องปิดเอง หลายคนเลี่ยงไปใช้ snprintf
เชื่อมสตริง
char buf[20] = "Hello";
strcat(buf, ", World"); // buf = "Hello, World" (ต้องมีที่พอ!)
// ปลอดภัยกว่า
strncat(buf, ", World", sizeof(buf) - strlen(buf) - 1);strcat เขียนต่อจาก '\0' เดิม โดยไม่เช็คขนาด — buffer overflow ถ้า buf เล็กไป
เปรียบเทียบสตริง
char a[] = "apple";
char b[] = "banana";
if (strcmp(a, b) == 0) { // 0 = เท่ากัน (ไม่ใช่ 1!)
printf("เหมือนกัน\n");
}
// strcmp คืน: <0 ถ้า a มาก่อน, 0 ถ้าเท่ากัน, >0 ถ้า a มาหลัง
// เทียบทีละไบต์ตามค่าของ unsigned char (บนเครื่องที่ใช้ ASCII ก็คือลำดับ ASCII)
printf("%d\n", strcmp("apple", "apple")); // 0
printf("%d\n", strcmp("apple", "banana")); // ค่าลบ
// ❌ อย่าเทียบสตริงด้วย ==
if (a == b) { ... } // เทียบที่อยู่ pointer ไม่ใช่เนื้อหา!strcmp คืน 0 เมื่อเท่ากัน — ต่างจากที่คาด if (strcmp(a, b)) แปลว่า “ไม่เท่ากัน”
ฟังก์ชันสตริงอื่นที่ใช้บ่อย
char text[] = "Hello, World";
char *p = strchr(text, 'W'); // หา char แรก คืน pointer หรือ NULL
char *q = strstr(text, "World"); // หา substring คืน pointer หรือ NULL
char copy[50];
strcpy(copy, text);
char *tok = strtok(copy, ", "); // แยกด้วย delimiter (แก้ copy ในตัว!)
while (tok != NULL) {
printf("[%s]\n", tok);
tok = strtok(NULL, ", ");
}<ctype.h> สำหรับตัวอักษรเดี่ยว: isdigit, isalpha, isspace, toupper, tolower
แปลงสตริง ↔ ตัวเลข
#include <stdlib.h>
int n = atoi("42"); // 42 (ไม่บอก error — คืน 0 ถ้า parse ไม่ได้)
#include <errno.h>
const char *s = "42abc";
char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (end == s) printf("ไม่มีตัวเลขเลย\n");
else if (*end != '\0') printf("มีอักขระเกิน: %s\n", end); // "abc"
else if (errno == ERANGE) printf("เกินช่วง long\n");
else printf("ได้ %ld\n", v);
char buf[20];
snprintf(buf, sizeof(buf), "%d", 12345); // int -> stringตรวจ error ของ strtol ให้ครบ 3 อย่าง: (1) end == ตัวสตริงต้นทาง = ไม่มีตัวเลข, (2) *end != '\0' = มีอักขระเกิน, (3) errno == ERANGE = เกินช่วง — ใช้ strtol ไม่ใช่ atoi เมื่อต้องตรวจ input
เรื่องที่คนมักพลาด
- ลืมว่าสตริงต้องมีที่สำหรับ
'\0'— เก็บ “Hello” ต้องมีอาร์เรย์ ≥ 6 ไบต์ strcpy/strcat/sprintfโดยไม่เช็คขนาดปลายทาง — buffer overflow ช่องโหว่คลาสสิก ใช้snprintfif (strcmp(a, b))คิดว่าเช็ค “เท่ากัน” — จริง ๆ เช็ค “ไม่เท่ากัน” ต้อง== 0- เทียบสตริงด้วย
a == b— เทียบที่อยู่ ใช้strcmp - แก้ string literal —
char *s = "hi"; s[0] = 'H';— UB ประกาศchar s[] = "hi"ถ้าจะแก้ strncpyแล้วคิดว่าปิด'\0'ให้เสมอ — ไม่ ถ้า src ยาว ≥ n ต้องปิดเองstrtokกับ string literal หรือสตริงที่ต้องใช้ต่อ — มันแก้ในตัว ทำสำเนาก่อนchar c = 'A'; strlen(&c);—&cไม่ใช่สตริง ไม่มี'\0'— over-read
สรุป
- สตริง = อาร์เรย์
charปิดท้ายด้วย'\0'— ทุกฟังก์ชันอ่านจนเจอ'\0' char s[] = "..."แก้ได้ (มีที่เก็บของเรา);const char *s = "..."แก้ไม่ได้ (literal)strlenไม่นับ'\0'และ O(n) — เก็บผลไว้ ไม่เรียกในเงื่อนไข loop- คัดลอก/เชื่อม: ใช้
snprintf/strncatที่จำกัดขนาด ไม่ใช่strcpy/strcatดิบ strcmp(a, b) == 0= เท่ากัน; อย่าใช้==เทียบสตริง- แปลงตัวเลข:
strtolดีกว่าatoi— ตรวจend == s(ไม่มีตัวเลข),*end(อักขระเกิน),errno == ERANGE(เกินช่วง)
บทถัดไปเข้าเรื่อง pointer เต็ม ๆ — &, *, ชนิดของ pointer และ NULL