Menu

Hàm xử lý chuỗi trong C: strlen, strcpy, strcat, strcmp và các hàm khác

Tìm hiểu <string.h> qua ví dụ thực tế: đo độ dài bằng strlen, sao chép bằng strcpy và strncpy, nối chuỗi bằng strcat, so sánh bằng strcmp, tìm kiếm bằng strchr và strstr, và cách tính kích thước buffer để không hàm nào ghi tràn.

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.

Mọi hàm trong <string.h> đều là một vòng lặp nhỏ chạy trên mảng char và dừng ở ký tự kết thúc null. Khi đã hiểu điều đó, thư viện này không còn là một danh sách tên khó hiểu mà trở thành vài vòng lặp bạn hoàn toàn có thể tự viết. Trang này trình bày những hàm gánh gần như toàn bộ công việc thực tế, cùng các quy tắc kích thước giúp chúng không ghi vượt quá cuối buffer.

Mọi thứ ở đây chỉ cần một dòng include:

#include <string.h>

strlen: có bao nhiêu ký tự

strlen đếm số byte cho đến ký tự kết thúc, không tính ký tự kết thúc. Hàm trả về size_t, nên hãy in bằng %zu.

strlen là 6; sizeof là 32. Giá trị thứ nhất là văn bản, giá trị thứ hai là vùng lưu trữ. Xem trang về chuỗi để hiểu vì sao sự phân biệt này quan trọng đến vậy.

strlen duyệt toàn bộ chuỗi mỗi lần gọi, nên đừng đặt nó trong điều kiện vòng lặp (loop) khi chuỗi không thay đổi:

/* re-counts the whole string on every iteration */
for (size_t i = 0; i < strlen(s); i++) { ... }

/* count once */
size_t n = strlen(s);
for (size_t i = 0; i < n; i++) { ... }

strcpy và strncpy: sao chép

strcpy(dst, src) sao chép các ký tự cùng ký tự kết thúc. Hàm không biết dst lớn bao nhiêu, nên nơi gọi phải đảm bảo dst chứa được ít nhất strlen(src) + 1 byte.

strncpy nhận số lượng tối đa, nhưng có một cái bẫy nổi tiếng: khi chuỗi nguồn dài ít nhất n byte, nó sao chép đúng n ký tự và không có ký tự kết thúc. Luôn tự thêm ký tự kết thúc:

sizeof dst - 1 cộng với ký tự kết thúc viết tường minh là mẫu cần ghi nhớ. (strncpy còn đệm số 0 vào các chuỗi nguồn ngắn cho đủ n byte, rất phí công với buffer lớn: hàm này được thiết kế cho bản ghi có độ rộng cố định, không phải để đảm bảo an toàn.)

strcat và strncat: nối chuỗi

strcat(dst, src) nối src vào cuối nội dung đang có trong dst. Đích phải chứa được cả hai chuỗi cộng một ký tự kết thúc, và phải đang chứa một chuỗi hợp lệ: nối vào vùng nhớ chưa khởi tạo là undefined behavior.

Chú ý char path[64] = "/home/ada"; thay vì char path[64];: chính phần khởi tạo khiến nó là một chuỗi hợp lệ cho lần gọi strcat đầu tiên.

strncat(dst, src, n) nối tối đa n ký tự và luôn thêm ký tự kết thúc, nên n là chỗ trống còn lại chứ không phải tổng kích thước buffer:

strncat(dst, src, sizeof dst - strlen(dst) - 1);

Mỗi lần gọi strcat lại quét đích từ đầu để tìm điểm cuối. Ghép một chuỗi dài trong vòng lặp theo cách đó có độ phức tạp bậc hai; với dữ liệu lớn, hãy tự theo dõi vị trí ghi hoặc dùng snprintf, được trình bày trong trang chuyển đổi chuỗi.

strcmp: so sánh

== so sánh địa chỉ, nên cho ra false với hai mảng khác nhau dù chứa cùng nội dung. strcmp so sánh các ký tự và trả về dấu của độ chênh lệch.

Ba quy tắc:

  • Kiểm tra bằng nhau bằng strcmp(a, b) == 0. Viết if (strcmp(a, b)) nghĩa là "nếu chúng khác nhau", và gần như ai cũng đọc ngược.
  • Chỉ dấu của kết quả là được định nghĩa. Đừng so sánh kết quả với 1 hay -1.
  • Thứ tự dựa trên giá trị byte, nên "Zebra" đứng trước "apple" trong ASCII. Để so sánh không phân biệt hoa thường, hãy chuyển cả hai bản sao về chữ thường trước: strcasecmp hay gặp là phần mở rộng của POSIX, không thuộc chuẩn C.

strncmp(a, b, n) chỉ so sánh n ký tự đầu, đây là cách gọn gàng để kiểm tra tiền tố:

strchr và strstr: tìm kiếm

strchr(s, ch) tìm lần xuất hiện đầu tiên của một ký tự; strrchr tìm lần cuối. strstr(haystack, needle) tìm một chuỗi con. Cả ba đều trả về con trỏ trỏ vào chuỗi gốc, hoặc NULL khi không tìm thấy.

Lấy con trỏ trả về trừ đi điểm đầu chuỗi sẽ được chỉ số; kiểu của nó là ptrdiff_t, in bằng %td. Vì kết quả trỏ vào bên trong mảng gốc, at + 1 chính là phần còn lại của chuỗi mà không cần sao chép gì: một thành ngữ rất phổ biến trong C.

Luôn kiểm tra NULL trước khi giải tham chiếu. strchr(email, '@') + 1 trên một chuỗi không có @ sẽ tính địa chỉ từ NULL, và chương trình không còn được định nghĩa nữa.

Tách chuỗi

C không có hàm split, nhưng strchr cộng một ký tự kết thúc là đủ để tách ngay tại chỗ. Ví dụ này cắt một địa chỉ email thành phần người dùng và tên miền:

memcpy sao chép đúng số byte chỉ định mà không quan tâm đến ký tự kết thúc, đúng thứ bạn cần khi đã biết độ dài. Sau đó ký tự kết thúc được viết thủ công.

Ví dụ hoàn chỉnh: chuẩn hóa tên

Kết hợp mọi thứ ở trên: cắt khoảng trắng, so sánh, sao chép và nối chuỗi:

Mọi lần ghi đều bị giới hạn bởi chính sizeof của đích, và mọi strncpy đều đi kèm một ký tự kết thúc viết tường minh. Áp dụng kỷ luật đó một cách nhất quán là điều làm cho code xử lý chuỗi trong C an toàn.

Các quy tắc kích thước cần nhớ

  • strcpy cần strlen(src) + 1 byte trống ở đích.
  • strcat cần strlen(dst) + strlen(src) + 1.
  • Sau strncpy, tự viết dst[n - 1] = '\0'.
  • Với strncat, số lượng là chỗ trống còn lại: sizeof dst - strlen(dst) - 1.
  • Kiểm tra strchr và strstr có trả về NULL không trước khi dùng kết quả.
  • Chỉ dùng sizeof dst khi dst là một mảng thật. Bên trong một hàm nhận char *dst, sizeof cho ra kích thước của con trỏ, nên hãy truyền độ dài buffer như một tham số riêng.

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

strcmp trong C trả về gì?

Trả về 0 khi hai chuỗi giống hệt nhau, một giá trị âm khi chuỗi thứ nhất đứng trước chuỗi thứ hai theo thứ tự sắp xếp, và một giá trị dương khi nó đứng sau. Chỉ dấu của kết quả là có ý nghĩa, đừng bao giờ giả định hàm trả về -1 hoặc 1. Phép kiểm tra bạn cần gần như luôn là if (strcmp(a, b) == 0).

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

a == b so sánh hai địa chỉ chứ không so sánh các ký tự. Hai mảng riêng biệt cùng chứa "cat" nằm ở hai địa chỉ khác nhau, nên phép so sánh cho ra false dù nội dung giống nhau. Hãy dùng strcmp(a, b) == 0 trong <string.h>.

strcpy và strncpy khác nhau thế nào?

strcpy sao chép cho đến '\0' của chuỗi nguồn mà không biết đích lớn bao nhiêu. strncpy dừng sau tối đa n byte, nhưng nếu chuỗi nguồn dài đến mức đó thì nó không sao chép ký tự kết thúc, nên bạn phải tự viết dst[n - 1] = '\0';. Không hàm nào tự động an toàn; việc kiểm soát kích thước là trách nhiệm của bạn.

Làm sao tìm chuỗi con trong C?

strstr(haystack, needle) trả về con trỏ tới lần xuất hiện đầu tiên, hoặc NULL nếu không có. Vì hàm trả về con trỏ trỏ vào chuỗi gốc, found - haystack cho bạn chỉ số. Dùng strchr khi bạn chỉ tìm một ký tự.

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