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 ช่องโหว่คลาสสิก ใช้ snprintf
  • if (strcmp(a, b)) คิดว่าเช็ค “เท่ากัน” — จริง ๆ เช็ค “ไม่เท่ากัน” ต้อง == 0
  • เทียบสตริงด้วย a == b — เทียบที่อยู่ ใช้ strcmp
  • แก้ string literalchar *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