Menu

Chuỗi trong C: mảng char, ký tự kết thúc null và ví dụ

C không có kiểu chuỗi: một chuỗi là mảng char kết thúc bằng byte '\0'. Trang này xây dựng cách hình dung đó, rồi trình bày chuỗi hằng, cách in, cách duyệt chuỗi và vì sao không thể sao chép chuỗi bằng =.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Hầu hết các ngôn ngữ cho bạn một kiểu string tự biết độ dài của nó. C thì không. Trong C, chuỗi (string) chỉ là một mảng char kèm theo một quy tắc: sau văn bản là một byte có giá trị 0, viết là '\0' và gọi là ký tự kết thúc null (null terminator). Mọi thứ khác về chuỗi trong C đều xuất phát từ quy ước duy nhất đó.

Chuỗi là mảng char cộng '\0'

Khai báo một chuỗi bằng cách khởi tạo mảng char từ một chuỗi hằng:

Mảng này gồm bốn byte, không phải ba:

name:  +-----+-----+-----+------+
       | 'A' | 'd' | 'a' | '\0' |
       +-----+-----+-----+------+
index:    0     1     2     3

sizeof(name) là 4. Trình biên dịch đếm các ký tự, cộng thêm một cho ký tự kết thúc và tự đặt kích thước mảng cho bạn. Byte thừa đó là lý do char name[3] = "Ada"; là một bug chực chờ xảy ra: văn bản vừa nhưng ký tự kết thúc thì không, nên không đoạn code nào phía sau biết chuỗi kết thúc ở đâu.

Khi bạn muốn có chỗ để thay đổi nội dung về sau, hãy tự ghi kích thước và chừa dư:

char name[32] = "Ada";   /* 3 characters, a terminator, 28 bytes spare */

Các byte không dùng được phần khởi tạo điền bằng 0, hoàn toàn vô hại.

Vì sao ký tự kết thúc quan trọng

Không hàm C nào nhận độ dài. printf("%s", s) chỉ được đưa một địa chỉ và cứ thế đi tới in từng byte cho đến khi gặp '\0'. Vì vậy một mảng char không có ký tự kết thúc thì không phải là chuỗi, và coi nó là chuỗi sẽ đọc bất cứ vùng nhớ nào nằm ngay sau đó.

Quy tắc cần nhớ: sizeof cho bạn vùng lưu trữ, strlen cho bạn văn bản. Với char buf[32] = "Ada", sizeof buf là 32 và strlen(buf) là 3. Chúng trả lời hai câu hỏi khác nhau, và nhầm lẫn giữa chúng là một trong những bug phổ biến nhất trong C.

Chuỗi hằng và mảng char

Hai dòng này trông giống nhau nhưng hoạt động rất khác nhau:

char  a[] = "hello";   /* an array you own, initialized from the literal */
char *p   = "hello";   /* a pointer aimed at the literal itself */

a là mảng 6 byte chứa bản sao riêng của văn bản. Bạn có thể sửa nó. p trỏ vào vùng dữ liệu chuỗi chỉ đọc của chương trình; chuỗi hằng được dùng chung, và ghi qua p là undefined behavior, thường làm chương trình crash lúc chạy chứ không báo lỗi lúc biên dịch.

Hãy viết const char * mỗi khi một con trỏ trỏ tới chuỗi hằng. Khi đó trình biên dịch sẽ từ chối lệnh ghi ngay lúc biên dịch thay vì để nó thành một lần crash. Chọn giữa hai cách thực chất là câu hỏi về con trỏ và mảng, được trình bày trong trang con trỏ và mảng.

Thêm một khác biệt đáng biết: sizeof a là 6, nhưng sizeof p là kích thước của một con trỏ (8 trên hầu hết hệ thống), bất kể văn bản dài bao nhiêu.

In và đọc chuỗi

%s in một chuỗi; %c in một ký tự.

Với đầu vào, hãy tránh hẳn gets: hàm này đã bị loại khỏi ngôn ngữ vì không thể dùng an toàn. Hãy dùng fgets, hàm nhận kích thước buffer và sẽ không ghi tràn:

fgets giữ lại ký tự xuống dòng bạn đã nhấn, điều gần như không bao giờ bạn muốn có trong chuỗi. Dòng strcspn tìm '\n' đầu tiên và ghi đè nó bằng ký tự kết thúc: một cách cắt bỏ gọn gàng và chuẩn mực.

Duyệt chuỗi từng ký tự

Vì điểm kết thúc được đánh dấu chứ không được đếm, vòng lặp (loop) chuẩn sẽ kiểm tra chính ký tự đó:

Điều kiện text[i] != '\0' thường được viết gọn thành text[i], vì '\0' bằng 0 và do đó là false. Cả hai đều đúng; dạng tường minh dễ đọc hơn khi bạn đang học.

Bạn cũng có thể duyệt bằng con trỏ thay vì chỉ số, cách này cũng chuẩn mực không kém trong C:

Ép kiểu sang unsigned char trước khi gọi toupper không phải để trang trí: các hàm trong <ctype.h> có hành vi không xác định với giá trị âm, và một char thường có thể mang giá trị âm với các byte lớn hơn 127.

Không thể sao chép chuỗi bằng =

Đây là bức tường mà người mới nào cũng đụng phải:

char a[10];
a = "hello";        /* error: assignment to expression with array type */

char b[10] = "hi";
char c[10];
c = b;              /* error - same reason */

Tên mảng không phải là một giá trị có thể sửa, nên = không có gì để làm. Chỗ duy nhất cách viết tắt hoạt động là khởi tạo khi khai báo, một thao tác khác do trình biên dịch thực hiện.

Để sao chép lúc chạy, hãy sao chép các byte bằng strcpy trong <string.h>:

Cùng lý do đó giải thích vì sao == không so sánh chuỗi. if (a == b) so sánh hai địa chỉ, nên cho ra false với hai mảng khác nhau dù chứa văn bản giống hệt. Hãy dùng strcmp(a, b) == 0. Cả hai hàm và các phiên bản an toàn hơn của chúng được trình bày trong trang hàm xử lý chuỗi.

Mảng chuỗi

Một danh sách chuỗi có thể là mảng con trỏ (cho văn bản cố định mà bạn chỉ đọc) hoặc mảng char hai chiều (khi từng phần tử cần sửa được):

Mảng con trỏ lưu ba địa chỉ trỏ vào các chuỗi hằng chỉ đọc: gọn nhẹ nhưng không sửa được. Dạng char[3][16] dành 48 byte trong vùng nhớ của riêng bạn, nên từng hàng đều có thể ghi lại.

Lỗi thường gặp

  • Quên byte của ký tự kết thúc. char s[5] = "hello" không còn chỗ cho '\0'. Hãy đặt kích thước bằng strlen + 1.
  • Dùng sizeof khi ý bạn là strlen. sizeof trên con trỏ cho ra kích thước con trỏ, không phải độ dài văn bản.
  • Ghi qua một char * trỏ tới chuỗi hằng. Hãy khai báo những con trỏ như vậy là const char *.
  • So sánh bằng ==. Phép này so sánh địa chỉ. Hãy dùng strcmp.
  • Đọc bằng scanf("%s", buf). Cách này không giới hạn kích thước và dễ dàng ghi tràn; nên dùng fgets.

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

Khai báo chuỗi trong C như thế nào?

Dùng một mảng char: char name[] = "Ada";. C không có kiểu chuỗi dựng sẵn: mảng đó chứa 'A', 'd', 'a' và một byte ẩn thứ tư là '\0', đánh dấu điểm kết thúc. Nếu muốn có chỗ để thay đổi nội dung về sau, hãy ghi rõ kích thước mảng: char name[32] = "Ada";.

Ký tự kết thúc null trong C là gì?

Là byte '\0' (giá trị 0) đánh dấu nơi chuỗi kết thúc. Mọi hàm chuẩn như printf("%s"), strlen, strcpy đều đọc tới cho đến khi gặp byte đó. Một mảng char không có byte này thì không phải là chuỗi, và truyền nó vào các hàm đó sẽ đọc vượt quá vùng nhớ của bạn.

Vì sao không gán chuỗi bằng = trong C được?

Vì mảng không phải là một giá trị có thể sao chép bằng =. char a[10]; a = "hi"; sẽ không biên dịch được. Thay vào đó, hãy sao chép các ký tự bằng strcpy(a, "hi") trong <string.h>, hoặc khởi tạo ngay khi khai báo: char a[10] = "hi";, đây là chỗ duy nhất cách viết tắt này hoạt động.

char * và char [] trong C khác nhau thế nào?

char s[] = "hi" tạo một mảng có thể sửa, chứa bản sao riêng của văn bản. char *p = "hi" tạo một con trỏ trỏ tới chuỗi hằng nằm trong vùng nhớ chỉ đọc: đọc thì không sao, ghi qua nó sẽ làm chương trình crash. Dùng mảng khi bạn định sửa, dùng con trỏ khi chỉ đọc.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu