0
0
Lập trình
TT

Hướng Dẫn Chi Tiết Về Chuỗi Trong C: Quy Tắc và Mẹo An Toàn

Đăng vào 8 tháng trước

• 7 phút đọc

Chủ đề:

KungFuTech

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

  1. Ba Hình Thức Của Chuỗi
  2. Ký Tự Kết Thúc Null
  3. Có Cần free Trước Khi Thay Đổi?
  4. Các Hàm Hỗ Trợ
  5. Trường Hợp Thực Tế
  6. Các Trường Hợp Lỗi Cần Tránh
  7. Mô Hình API: Copy-on-Set
  8. Lựa Chọn Giữa Mảng và Con Trỏ
  9. 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* p vì điều này được đánh dấu là UB.
  • Sử dụng thư viện C như sds nế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 Copy
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 Copy
free(p);
p = xstrdup(new_value);

4. Các Hàm Hỗ Trợ

xstrdup (không giới hạn độ dài)

c Copy
#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 Copy
#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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
void replace_owned_string(char **dst, const char *src) {
    char *copy = xstrdup(src);
    free(*dst);
    *dst = copy;
}

Sử dụng:

c Copy
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ư sds nế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 khi free().
  • 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ành NULL.
  • 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 khi free().
  • 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.
Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào