Bài 11.118 phút đọc
Chuỗi trong C
Sau bài này bạn sẽ làm được
- Phân biệt char s[] với char *s trên năm tiêu chí
- Biết chuỗi nào ghi được và chuỗi nào không
- Tính đúng kích thước mảng cần cấp cho một chuỗi
- Nhận ra ba lỗi chuỗi phổ biến nhất
C không có kiểu chuỗi. Nó có mảng ký tự và một quy ước: byte 0 đánh dấu chỗ kết thúc. Toàn bộ thư viện string.h, mọi lỗ hổng tràn bộ đệm trong lịch sử phần mềm, và mọi cái khó của chương này đều bắt nguồn từ quyết định thiết kế đó.
#C không có kiểu chuỗi
Chuỗi trong C
Một dãy ký tự liên tiếp trong bộ nhớ, kết thúc bằng byte có giá trị 0. Không có kiểu riêng, không có trường độ dài, không có gì ngoài mảng ký tự và quy ước.
char s[6] = "hello";
/* Trong bộ nhớ:
'h' 'e' 'l' 'l' 'o' '\0'
104 101 108 108 111 0 <- mã ASCII của từng ô */| Ngôn ngữ khác có | C có gì thay thế |
|---|---|
| Kiểu string riêng | Mảng char, hoặc con trỏ char |
| Trường độ dài lưu sẵn | Phải đếm tới byte 0, tức hàm strlen |
| Nối bằng dấu cộng | Hàm strcat, và bạn phải tự lo chỗ trống |
| So sánh bằng dấu bằng kép | Hàm strcmp |
| Tự cấp phát khi chuỗi dài ra | Bạn tự cấp, tự giải phóng |
| Kiểm tra biên | Không có gì cả |
#Hai cách khai báo, hai thứ khác nhau
char s[] = "abc"; /* mảng ký tự, ghi được */
char *p = "abc"; /* con trỏ tới hằng chuỗi, KHÔNG ghi được */hai-cach.c
#include <stdio.h>
int main(void)
{
char s[] = "abc";
char *p = "abc";
printf("sizeof s = %zu\n", sizeof s); /* 4, gồm cả byte kết thúc */
printf("sizeof p = %zu\n", sizeof p); /* 8, kích thước một con trỏ */
s[0] = 'A'; /* hợp lệ */
printf("%s\n", s); /* Abc */
p[0] = 'A'; /* chương trình sập tại đây */
return 0;
}terminal
gcc -std=c17 -Wall hai-cach.c -o t && ./t
sizeof s = 4 sizeof p = 8 Abc Segmentation fault (core dumped)
#Bảng so sánh đầy đủ
| Tiêu chí | char s[] = "abc" | char *p = "abc" |
|---|---|---|
| Bản chất | Mảng 4 ký tự | Con trỏ trỏ tới hằng chuỗi |
| Nằm ở đâu | Ngăn xếp, hoặc .data nếu toàn cục | Con trỏ trên ngăn xếp, nội dung ở .rodata |
| Ghi được không | Có | Không, hành vi không xác định |
| sizeof cho ra | 4, tức độ dài cộng một | 8, kích thước con trỏ |
| Gán lại được không | Không, s không phải biến gán được | Có, p trỏ đi chỗ khác được |
| Chi phí khởi tạo | Chép 4 byte mỗi lần vào khối | Chép một địa chỉ |
| Dùng khi nào | Cần sửa nội dung chuỗi | Chỉ đọc, hoặc trỏ vào chuỗi có sẵn |
char s[] = "abc";
char *p = "abc";
s = "xyz"; /* lỗi biên dịch: mảng không gán được */
p = "xyz"; /* hợp lệ: con trỏ trỏ sang hằng chuỗi khác */
s[0] = 'A'; /* hợp lệ */
p[0] = 'A'; /* hành vi không xác định */#Tính kích thước mảng cần cấp
Sai lầm số một của người mới là quên byte kết thúc khi tính kích thước.
char s1[6] = "hello"; /* đúng: 5 ký tự cộng 1 byte kết thúc */
char s2[5] = "hello"; /* KHÔNG có byte kết thúc, không phải chuỗi hợp lệ */
char s3[4] = "hello"; /* lỗi biên dịch: initializer-string too long */| Chuỗi | Số ký tự | Số ô mảng cần |
|---|---|---|
| "" | 0 | 1 |
| "a" | 1 | 2 |
| "hello" | 5 | 6 |
| "xin chao" | 8 | 9 |
Đếm tay, dễ lệch một
char ten[8] = "Nguyen Van A"; /* thiếu chỗ, cắt cụt */
char lop[5] = "CNTT1"; /* vừa khít nhưng mất byte kết thúc */Để trình biên dịch đếm
char ten[] = "Nguyen Van A"; /* trình biên dịch tự đếm ra 13 */
char lop[] = "CNTT1"; /* tự đếm ra 6 */
/* Khi cần chỗ cho nội dung sẽ thay đổi, cấp thừa hẳn ra */
char ho_ten[64] = "Nguyen Van A";#Ba lỗi chuỗi phổ biến nhất
Lỗi 1: quên byte kết thúc
char s[5];
s[0] = 'h'; s[1] = 'e'; s[2] = 'l'; s[3] = 'l'; s[4] = 'o';
printf("%s\n", s); /* in ra "hello" rồi in tiếp rác cho tới khi gặp byte 0 */Lỗi 2: dùng ký tự thay cho chuỗi
char c = 'a'; /* một ký tự, chiếm 1 byte */
char s[] = "a"; /* một chuỗi, chiếm 2 byte */
printf("%s\n", c); /* SAI: %s cần con trỏ, c là số 97, chương trình sập */
printf("%c\n", s); /* SAI: %c cần ký tự, s là địa chỉ */
printf("%c\n", c); /* đúng */
printf("%s\n", s); /* đúng */Lỗi 3: ghi vào hằng chuỗi
void viet_hoa(char *s)
{
for (size_t i = 0; s[i] != '\0'; ++i)
if (s[i] >= 'a' && s[i] <= 'z')
s[i] -= 32;
}
char t[] = "hello";
viet_hoa(t); /* đúng, t là mảng ghi được */
viet_hoa("hello"); /* chương trình sập */Hàm này không có cách nào biết được nó đang nhận mảng ghi được hay hằng chuỗi. Trách nhiệm hoàn toàn thuộc về người gọi. Đó là lý do quy ước const char * cho tham số chỉ đọc quan trọng tới vậy: nó biến trách nhiệm thành thứ trình biên dịch kiểm tra được.
Tự làm thử
- In
sizeofcủachar s[] = "abc"vàchar *p = "abc", giải thích hai con số. - Thử ghi vào cả hai, quan sát cái nào chạy và cái nào sập, rồi bật
-Wwrite-stringsđể bắt lỗi từ lúc biên dịch. - Khai báo
char s[5] = "hello"rồi gọistrlenvàprintfvới%s. Chạy dưới-fsanitize=address. - So sánh hai hằng chuỗi giống nhau bằng toán tử bằng, chạy trên hai trình biên dịch khác nhau và so kết quả.
- In
sizeof('a')vàsizeof("a"), giải thích chênh lệch. - Viết hàm chuyển chuỗi thành chữ hoa, gọi nó với mảng và với hằng chuỗi, quan sát kết quả.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- C không có kiểu chuỗi. Chuỗi là mảng ký tự kết thúc bằng byte 0, và mọi thứ khác đều là quy ước.
char s[] = "abc"tạo mảng ghi được,char *p = "abc"chỉ trỏ tới vùng chỉ đọc.sizeofmảng cho ra độ dài cộng một,sizeofcon trỏ luôn là 8 trên máy sáu tư bit.- Chuỗi n ký tự cần mảng
n + 1ô. Quên byte kết thúc là lỗi phổ biến nhất. - Dùng
const char *cho mọi tham số chuỗi chỉ đọc, và bật-Wwrite-stringscùng-fsanitize=address.