← Lập trình C nâng cao

Bài 5 · Nâng cao · 22 phút· Cập nhật 11/06/2026

Chuỗi & con trỏ char

Biên soạn bởi Nguyễn Anh Tuấn

Chuỗi và con trỏ char trong C (string & char pointer): char arr[] vs char *p, chuỗi hằng chỉ-đọc, duyệt chuỗi bằng con trỏ và tự viết strlen.

Nhắc lại từ khoá cơ bản: chuỗi là mảng char kết thúc bằng \0. Ghép với bài trước: tên mảng là địa chỉ phần tử đầu, nên một char * có thể trỏ thẳng vào chuỗi:

chuoi-con-tro.c

#include <stdio.h>

int main() {
    char s[] = "Hi";     // {'H','i','\0'}
    char *p = s;         // p tro toi 'H' (s[0])

    printf("%c\n", *p);  // H   - ky tu dau
    printf("%s\n", p);   // Hi  - in tu p toi '\0'
    return 0;
}

Kết quả khi chạy

H
Hi
  • Chuỗi = mảng char + ký tự kết thúc \0.
  • char *p = s; cho p trỏ tới ký tự đầu (không cần &).
  • %c in một ký tự (*p); %s in cả chuỗi từ p tới \0.

Đây là khác biệt quan trọng nhất của bài. Mảng tạo một bản sao sửa được; con trỏ tới chuỗi trong nháy kép trỏ vào vùng chỉ-đọc:

array-vs-literal.c

#include <stdio.h>

int main() {
    char arr[] = "Hello";   // BAN SAO, sua duoc (tren stack)
    char *lit  = "Hello";   // tro toi chuoi HANG (read-only)

    arr[0] = 'J';           // OK  -> "Jello"
    // lit[0] = 'J';        // UB! sua chuoi hang -> co the sap

    printf("%s\n", arr);    // Jello
    return 0;
}

Kết quả khi chạy

Jello

Quy tắc

Muốn sửa chuỗi → dùng char arr[] = "..." (bản sao). Chỉ đọcconst char *p = "..." (thêm const để trình biên dịch chặn ghi nhầm). Đừng bao giờ ghi qua một con trỏ trỏ tới string literal.

Vì chuỗi kết thúc bằng \0, ta duyệt nó bằng cách cho con trỏ chạy p++ cho tới khi *p == '\0'. Bấm Bước (p++) để xem con trỏ đi và đếm độ dài:

char s[] = "Code";
▼ p
C
0x100
o
0x101
d
0x102
e
0x103
\0
0x104
*p = 'C'
int n = 0;
while (*p != '\0') { n++; p++; }
n = 0 · con trỏ đang ở 0x100, chưa gặp '\0'.

char * nhảy 1 byte mỗi bước (sizeof char), nên duyệt chuỗi chỉ là p++ cho tới khi *p bằng '\0'.

do-dai.c

#include <stdio.h>

int doDai(char *p) {
    int n = 0;
    while (*p != '\0') {   // chua gap ky tu ket thuc
        n++;
        p++;               // tien toi ky tu ke (1 byte)
    }
    return n;
}

int main() {
    printf("%d\n", doDai("Hello"));  // 5
    return 0;
}

Kết quả khi chạy

5

Hiểu cơ chế trên thì tự viết được những hàm chuỗi quen thuộc. Chép một chuỗi là duyệt src tới \0, chép từng ký tự, rồi tự thêm \0 ở cuối:

chep.c

#include <stdio.h>

void chep(char *dst, char *src) {
    while (*src != '\0') {
        *dst = *src;   // chep ky tu
        dst++;
        src++;
    }
    *dst = '\0';       // DUNG QUEN: dau ket thuc chuoi
}

int main() {
    char buf[10];
    chep(buf, "Hi");
    printf("%s\n", buf);   // Hi
    return 0;
}

Kết quả khi chạy

Hi
  • char* duyệt chuỗi: p++ nhảy 1 byte mỗi bước (sizeof char).
  • Thành ngữ c = *p++; đọc *p rồi tiến p - rất hay gặp trong code C.
  • Tự dựng chuỗi thì PHẢI tự đặt \0 ở cuối, nếu không chuỗi sẽ không có điểm dừng.

Ba lỗi chuỗi-con-trỏ nguy hiểm nhất:

  • Sửa string literal: char *p = "abc"; p[0] = 'X'; → hành vi không xác định.
  • Quên \0 khi tự dựng chuỗi → printf/strlen đọc tràn sang vùng nhớ lạ.
  • char *p chưa trỏ tới buffer hợp lệ rồi ghi vào → con trỏ hoang, hỏng bộ nhớ.

An toàn bộ đệm

strcpy/strcat không kiểm tra kích thước đích → dễ tràn bộ đệm (buffer overflow), một lỗ hổng bảo mật kinh điển. Khi có thể, ưu tiên snprintf hoặc strncpy và luôn chừa chỗ cho \0.

Câu hỏi thường gặp

arr là một MẢNG: trình biên dịch sao chép "Hi\0" vào vùng nhớ của arr (thường trên stack), bạn sửa thoải mái. p là CON TRỎ trỏ tới một chuỗi HẰNG nằm ở vùng chỉ-đọc; sửa qua p (p[0] = ...) là hành vi không xác định, thường gây sập.

Nó in từng ký tự kể từ địa chỉ p cho tới khi gặp \0. Không có \0 thì printf cứ đọc tràn sang vùng nhớ kế tiếp - in ra rác và có thể sập. Đó là lý do \0 sống còn.

1 byte, vì sizeof(char) = 1. Nhờ vậy duyệt chuỗi rất gọn: chỉ p++ để sang ký tự kế. (So với int* nhảy 4 byte - xem bài Con trỏ & mảng.)

*(p++): lấy giá trị *p TRƯỚC, rồi tăng p. Một thành ngữ C kinh điển để vừa đọc ký tự vừa tiến con trỏ: c = *p++;. Đừng nhầm với (*p)++ (tăng giá trị được trỏ tới).

Duyệt src từng ký tự tới \0, chép sang dst, rồi TỰ thêm \0 ở cuối dst. Hàm chuẩn là strcpy(dst, src) - nhưng nó không kiểm tra dst đủ chỗ; thực tế nên dùng snprintf để tránh tràn bộ đệm (cẩn thận với strncpy: khi cắt ngắn nó KHÔNG tự thêm \0).

Chuỗi hằng (string literal) thường nằm ở vùng nhớ CHỈ ĐỌC của chương trình. Ghi vào đó (qua char *p = "Hello"; p[0] = ...) là hành vi không xác định - máy có thể chặn và gây segfault. Muốn sửa, hãy dùng char arr[] = "Hello"; (bản sao).

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

char arr[] = "Hi";char *p = "Hi"; khác nhau căn bản ở chỗ nào?

  1. 1

    Đếm bằng con trỏ

    Dùng công cụ ở Bước 3, gõ "Code" rồi bấm p++ tới hết. Độ dài là bao nhiêu? Con trỏ dừng ở ô có giá trị gì?

    Hoàn thành khi: Độ dài 4. Con trỏ dừng ở ô \0 (mã ASCII 0) - vòng while dừng khi *p == \0.

  2. 2

    Ký tự đầu

    Cho char s[] = "abc"; char *p = s; thì *p bằng ký tự nào? Mã ASCII bao nhiêu?

    Hoàn thành khi: *p = 'a' (mã 97) - p trỏ tới phần tử đầu s[0].

  3. 3

    Bước nhảy của char*

    Mỗi p++ với char* nhảy bao nhiêu byte? Vì sao khác int*?

    Hoàn thành khi: 1 byte (sizeof char = 1). int* nhảy 4 vì sizeof int = 4.

  4. 4

    Tự viết strlen

    Viết hàm int doDai(char *p) đếm độ dài chuỗi bằng con trỏ, KHÔNG dùng strlen.

    Hoàn thành khi: int n = 0; while (*p != '\0') { n++; p++; } return n;

  5. 5

    Sửa được hay không?

    char arr[] = "Hi"; arr[0] = 'J'; có hợp lệ? Còn char *p = "Hi"; p[0] = 'J'; thì sao?

    Hoàn thành khi: arr OK (bản sao, sửa được) → "Ji". p là chuỗi hằng read-only ⇒ hành vi không xác định (thường sập).

  6. 6

    Vì sao cần \0

    Khi tự chép chuỗi qua con trỏ, vì sao bắt buộc thêm \0 ở cuối đích?

    Hoàn thành khi: Không có \0, printf/strlen không biết chuỗi kết thúc ở đâu → đọc tràn sang vùng nhớ kế tiếp.