Bỏ qua điều hướng, tới nội dung chính
Học C
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êngMảng char, hoặc con trỏ char
Trường độ dài lưu sẵnPhải đếm tới byte 0, tức hàm strlen
Nối bằng dấu cộngHàm strcat, và bạn phải tự lo chỗ trống
So sánh bằng dấu bằng képHàm strcmp
Tự cấp phát khi chuỗi dài raBạn tự cấp, tự giải phóng
Kiểm tra biênKhô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 */
Bên trái nội dung được chép vào một mảng riêng trên ngăn xếp. Bên phải chỉ có địa chỉ được chép, còn nội dung nằm ở vùng chỉ đọc dùng chung.
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ấtMảng 4 ký tựCon trỏ trỏ tới hằng chuỗi
Nằm ở đâuNgăn xếp, hoặc .data nếu toàn cụcCon trỏ trên ngăn xếp, nội dung ở .rodata
Ghi được khôngKhông, hành vi không xác định
sizeof cho ra4, tức độ dài cộng một8, kích thước con trỏ
Gán lại được khôngKhông, s không phải biến gán đượcCó, p trỏ đi chỗ khác được
Chi phí khởi tạoChép 4 byte mỗi lần vào khốiChép một địa chỉ
Dùng khi nàoCần sửa nội dung chuỗiChỉ đọ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ỗiSố ký tựSố ô mảng cần
""01
"a"12
"hello"56
"xin chao"89
Đế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ử

  1. In sizeof của char s[] = "abc" char *p = "abc", giải thích hai con số.
  2. 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.
  3. Khai báo char s[5] = "hello" rồi gọi strlen printf với %s. Chạy dưới -fsanitize=address.
  4. 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ả.
  5. In sizeof('a')sizeof("a"), giải thích chênh lệch.
  6. 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.
  • sizeof mảng cho ra độ dài cộng một, sizeof con 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-strings cùng -fsanitize=address.