Giới thiệu
Trong lập trình C, việc xử lý chuỗi có thể là một thách thức lớn, đặc biệt là khi bạn chuyển từ các ngôn ngữ cao cấp hơn như C++. Ngôn ngữ C không có std::string, mà chỉ sử dụng con trỏ và mảng để quản lý chuỗi. Điều này có nghĩa là bạn cần tự mình quản lý việc cấp phát, thay đổi, và giải phóng bộ nhớ. Hướng dẫn này sẽ giúp bạn hiểu rõ hơn về các quy tắc an toàn khi làm việc với chuỗi trong C, giúp bạn tránh được những lỗi phổ biến.
Mục lục
- Ba Hình Thức Của Chuỗi
- Ký Tự Kết Thúc Null
- Có Cần
freeTrước Khi Thay Đổi? - Các Hàm Hỗ Trợ
- Trường Hợp Thực Tế
- Các Trường Hợp Lỗi Cần Tránh
- Mô Hình API: Copy-on-Set
- Lựa Chọn Giữa Mảng và Con Trỏ
- Danh Sách Kiểm Tra Nhanh
1. Ba Hình Thức Của Chuỗi
| Hình Thức | Lưu Trữ | Có thể thay đổi nội dung? | Có thể gán lại con trỏ? | Cần free? |
|---|---|---|---|---|
const char *p = "abc"; |
literal | ❌ (chỉ đọc) | ✅ có | ❌ không |
char buf[16] = "abc"; |
mảng | ✅ (tại chỗ) | ❌ (mảng không thể gán lại) | ❌ không |
char *p = xstrdup("abc"); |
heap | ✅ (nếu dung lượng phù hợp) | ✅ có | ✅ có |
Quy Tắc Chung
- Literal: mượn, không thay đổi, sống mãi.
- Mảng: nhúng, có thể thay đổi, không cần giải phóng.
- Con trỏ heap: sở hữu, có thể thay đổi, cần giải phóng.
- char *p: Trong hầu hết API C, KHÔNG GÁN CHO LITERAL nếu bạn thấy trường
char* pvì điều này được đánh dấu là UB. - Sử dụng thư viện C như
sdsnếu bạn không muốn quản lý chuỗi thủ công.
2. Ký Tự Kết Thúc Null
Ký Tự Kết Thúc
- Literal: luôn kết thúc bằng null.
- Mảng: phải có chỗ cho
\0. - Chuỗi từ heap: luôn cấp phát +1 cho ký tự kết thúc.
✅ Luôn sử dụng snprintf cho các ghi an toàn và có giới hạn:
c
char buf[16];
snprintf(buf, sizeof buf, "%s", "127.0.0.1"); // tự động thêm NUL
3. Có Cần free Trước Khi Thay Đổi?
| Trường Hợp | Hành Động | Cần giải phóng trước? |
|---|---|---|
Mảng char buf[N] |
ghi đè tại chỗ | ❌ không bao giờ |
| Con trỏ → literal/mượn | gán lại con trỏ | ❌ không bao giờ |
| Con trỏ → heap (sở hữu) | ghi đè (nếu đủ dung lượng) | ❌ không |
| Con trỏ → heap (sở hữu) | thay thế (cấp phát mới) | ✅ có |
➡️ Nếu không chắc về dung lượng → an toàn nhất là:
c
free(p);
p = xstrdup(new_value);
4. Các Hàm Hỗ Trợ
xstrdup (không giới hạn độ dài)
c
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* Nhân bản chuỗi C:
- s == NULL -> trả về "" sở hữu.
- PHẢI LÀ chuỗi C hợp lệ với ký tự kết thúc \0 (hợp đồng của caller).
*/
static inline char *xstrdup(const char *s) {
if (!s) {
char *z = malloc(1);
if (!z){
return NULL;
}
z[0] = '\0'; return z;
}
size_t n = strlen(s);
char *p = malloc(n + 1);
if (!p){
return NULL;
}
memcpy(p, s, n + 1);
return p;
}
xstrndup (giới hạn độ dài)
c
#include <stdlib.h>
#include <string.h>
#include <limits.h>
/* Chính sách: NULL hoặc maxlen==0 -> trả về chuỗi trống sở hữu */
static inline char *xstrndup(const char *s, size_t maxlen) {
if (s == NULL || maxlen == 0) {
char *z = malloc(1);
if (!z) {
return NULL;
}
z[0] = '\0';
return z;
}
/* Bảo vệ tràn (maxlen + 1) */
if (maxlen == SIZE_MAX) {
return NULL;
}
size_t n = strnlen(s, maxlen);
char *p = malloc(n + 1);
if (!p) {
return NULL;
}
if (n) {
memcpy(p, s, n);
}
p[n] = '\0';
return p;
}
5. Trường Hợp Thực Tế
A) Con trỏ tới literal (KHÔNG SỬ DỤNG)
Mẫu này bạn phải tránh. Theo hầu hết hợp đồng API C, mẫu này không được phép vì nó sẽ gây nhầm lẫn khi giải phóng bộ nhớ.
c
char *p = "0.0.0.0"; // literal mượn
p = "127.0.0.1"; // ✅ gán lại cho literal khác
// p[0] = '1'; // ❌ UB: không thể sửa đổi literal
// free(p); // ❌ UB: không giải phóng literals
B) Literal trước, sau đó gán lại cho bản sao sở hữu
Mẫu này phải tránh khai báo char *p với chuỗi literal ngay từ đầu, nhưng nó an toàn.
c
char *p = "0.0.0.0"; // bắt đầu là literal mượn
p = xstrdup(p); // ✅ giờ đã sở hữu trên heap, phải giải phóng sau
if(p){
free(p); // ✅ giờ đã sở hữu trên heap, phải giải phóng sau
p = NULL;
}
C) Mảng bên trong cấu trúc
c
struct S { char host[16]; } s = {0};
snprintf(s.host, sizeof(s.host), "%s", "0.0.0.0");
snprintf(s.host, sizeof(s.host), "%s", "127.0.0.1"); // ✅ ghi đè tại chỗ
// không bao giờ giải phóng, bộ đệm thuộc về cấu trúc
D) Con trỏ sở hữu bên trong cấu trúc (copy-on-set)
c
struct S { char *host; } s = {0};
s.host = xstrdup("0.0.0.0");
char *tmp = xstrdup("127.0.0.1");
free(s.host);
s.host = tmp;
free(s.host);
s.host = NULL;
E) Con trỏ sở hữu (thay đổi tại chỗ với dung lượng)
c
char *p = malloc(64);
snprintf(p, 64, "%s", "0.0.0.0");
snprintf(p, 64, "%s", "127.0.0.1"); // ✅ vừa đủ
free(p);
p = NULL;
F) Bắt đầu là literal, sau đó sao chép, sau đó thay thế
c
char *p = "init"; // literal, mượn
p = xstrdup(p); // giờ đã sở hữu bản sao heap
replace_owned_string(&p, "newvalue"); // giải phóng + gán lại an toàn
free(p);
p = NULL;
6. Các Trường Hợp Lỗi Cần Tránh
| Lỗi | Mã xấu | Cách sửa |
|---|---|---|
| Sửa đổi literal | char *p="abc"; p[0]='A'; |
Sử dụng char buf[]="abc"; hoặc xstrdup("abc") |
| Giải phóng literal | char *p="abc"; free(p); |
Không bao giờ giải phóng literals |
| Giải phóng đôi | free(p); free(p); |
|
| Sử dụng sau khi giải phóng | free(p); p = NULL; printf("%s", p); |
Luôn đặt thành NULL, kiểm tra trước khi sử dụng |
| Tràn bộ đệm | strcpy(buf,"longstring"); |
Sử dụng snprintf(buf,sizeof buf,"%s",src) |
| Đọc chưa khởi tạo | char *p=malloc(16); if(p[0]=='a')... |
calloc hoặc memset trước khi đọc |
| Realloc UB | p=realloc(p,new); use(p+old..) |
Luôn memset vùng mới |
7. Mô Hình API: Copy-on-Set
c
void replace_owned_string(char **dst, const char *src) {
char *copy = xstrdup(src);
free(*dst);
*dst = copy;
}
Sử dụng:
c
struct S { char *host; } s = {0};
replace_owned_string(&s.host, "0.0.0.0");
replace_owned_string(&s.host, "127.0.0.1");
free(s.host);
s.host = NULL;
8. Lựa Chọn Giữa Mảng và Con Trỏ
- Mảng (
char field[N]): trường cố định kích thước, các phần tử dữ liệu ISO8583, IPv4, dấu thời gian. - Con trỏ (
char *field): đầu vào không giới hạn, dữ liệu do người dùng cung cấp.
➡️ Mảng = đơn giản hơn, không cần free.
➡️ Con trỏ = linh hoạt, nhưng bạn phải giải phóng hoặc thay thế cẩn thận.
9. Danh Sách Kiểm Tra Nhanh
- Sử dụng thư viện C như
sdsnếu bạn không muốn quản lý chuỗi thủ công. - KHÔNG sử dụng literal trên
char* pđể tránh nhầm lẫn trong khifree(). - Literals: chỉ gán lại con trỏ; không sửa đổi/giải phóng.
- Mảng: sửa đổi tại chỗ với
snprintf; không cần giải phóng. - Sở hữu heap: sửa đổi nếu vừa đủ; nếu không, giải phóng cũ + sao chép mới.
- Nếu không chắc chắn, luôn sử dụng
replace_owned_string. - Sau khi
free()hãy đặt con trỏ thànhNULL. - Không bao giờ đánh giá bộ nhớ chưa khởi tạo.
Tổng Kết
- Theo tiêu chuẩn API C, KHÔNG sử dụng literal trên
char* pđể tránh nhầm lẫn trong khifree(). char *p = "value";→ con trỏ tới literal (mượn).- Gán lại cho literal khác là ổn.
- Gán lại cho
xstrdup("...")→ giờ đã sở hữu → phải giải phóng. - Mảng là an toàn, bộ đệm cố định.
- Con trỏ heap cần kiểm soát vòng đời thủ công.
- Mô hình copy-on-set giúp mã của bạn đơn giản và an toàn.