0
0
Lập trình
Harry Tran
Harry Tran106580903228332612117

Hướng dẫn Chiến lược Cấp phát Bộ nhớ An toàn & Hiện đại trong C

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

• 9 phút đọc

Hướng dẫn Chiến lược Cấp phát Bộ nhớ An toàn & Hiện đại trong C

C là một ngôn ngữ mạnh mẽ nhưng cũng đầy rủi ro nếu bạn không sử dụng đúng cách. Bài viết này sẽ tiêu chuẩn hóa cách chúng ta cấp phát, khởi tạo, sao chép và giải phóng bộ nhớ và chuỗi theo cách di động và an toàn (ISO C + các hàm trợ giúp nhỏ, có tài liệu). Chúng tôi sẽ tránh các hành vi không xác định (UB), những bất ngờ trên nền tảng và các cạm bẫy cấp phát bộ nhớ “ẩn”.

0) Quy tắc Tóm tắt

  • Cấp phát ≠ Khởi tạo. Không bao giờ đọc bộ nhớ chưa được khởi tạo.
  • Ưu tiên calloc cho các cấu trúc; nếu không thì malloc + khởi tạo rõ ràng (sử dụng memset hoặc khởi tạo theo từng trường).
  • Sau realloc, khởi tạo phần đuôi mới (byte vượt quá kích thước cũ).
  • Không bao giờ sử dụng strdup thô trong mã của chúng ta. Sử dụng xstrdup/xstrndup dưới đây.
  • Tập trung vào vòng đời với các API create/destroy. Tài liệu hóa quyền sở hữu (vay mượn so với sở hữu).
  • Sau khi free, đặt con trỏ thành NULL.

1) Các API Cấp phát Cơ bản

Hàm Mục đích Đã khởi tạo? Cần khởi tạo?
malloc(n) Cấp phát n byte (heap) ❌ rác ✅ memset hoặc gán
calloc(c,n) Cấp phát c*n byte (heap) ✅ được khởi tạo
realloc(p,n) Thay đổi kích thước khối p thành n byte 🔸 một phần¹ ✅ khởi tạo phần đuôi mới
alloca(n) Cấp phát n byte (stack, tự động giải phóng) ❌ rác ✅ memset hoặc gán

¹ realloc: giữ lại byte cũ; các byte mới không được khởi tạo.

Quy tắc vàng: Không bao giờ đánh giá bộ nhớ mà bạn chưa khởi tạo.


2) Các Wrapper Cấp phát An toàn (ISO C, OOM-fatal, OOM-NULL với *_try)

Sử dụng những hành vi nhất quán này:

c Copy
#ifndef SAFE_ALLOC_H
#define SAFE_ALLOC_H

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>

/* Chính sách Abort-on-OOM (Out of Memory) (thường dùng cho daemons/servers).
   Nếu bạn cần OOM không fatal, hãy sử dụng các biến thể *_try riêng biệt. */

static inline void *xmalloc(size_t size) {
    void *p = malloc(size);
    if (!p && size) {
        fprintf(stderr, "FATAL: malloc(%zu) failed\n", size);
        abort();
    }
    return p;
}

static inline void *xmalloc_try(size_t size) {
    void *p = malloc(size);
    if (!p && size) {
        return NULL;
    }
    return p;
}

static inline void *xcalloc(size_t nmemb, size_t size) {
    /* bảo vệ tràn cơ bản */
    if (size && nmemb > SIZE_MAX / size) {
        fprintf(stderr, "FATAL: calloc overflow (%zu,%zu)\n", nmemb, size);
        abort();
    }
    void *p = calloc(nmemb, size);
    if (!p && nmemb && size) {
        fprintf(stderr, "FATAL: calloc(%zu,%zu) failed\n", nmemb, size);
        abort();
    }
    return p;
}

static inline void *xcalloc_try(size_t nmemb, size_t size) {
    /* bảo vệ tràn cơ bản */
    if (size && nmemb > SIZE_MAX / size) {
        return NULL;
    }
    void *p = calloc(nmemb, size);
    if (!p && nmemb && size) {
        return NULL;
    }
    return p;
}

static inline void *xrealloc(void *ptr, size_t size) {
    void *p = realloc(ptr, size);
    if (!p && size != 0) {
        fprintf(stderr, "FATAL: realloc(%p,%zu) failed\n", ptr, size);
        abort();
    }
    return p;
}

static inline void *xrealloc_try(void *ptr, size_t size) {
    void *p = realloc(ptr, size);
    if (!p && size != 0) {
        return NULL;
    }
    return p;
}

/* Giải phóng + NULL 
*  Chúng tôi đặt phạm vi ở đây để tránh việc không sử dụng if-else trên câu lệnh không có dấu ngoặc nhọn
*/
#define xfree(p) { if ((p) != NULL) { free(p); p = NULL; } }

/* Sao chép chuỗi giới hạn: phải cung cấp chuỗi hợp lệ với NUL-terminate. */
static inline char *xstrdup(const char *s) {
    if (!s) {
        /* Xác định chính sách của chúng tôi: sao chép NULL → chuỗi rỗng */
        char *z = xmalloc(1);
        z[0] = '\0';
        return z;
    }
    size_t n = strlen(s);
    /* +1 kiểm tra tràn */
    if (n >= SIZE_MAX) {
        fprintf(stderr, "FATAL: xstrdup overflow\n");
        abort();
    }

    char *p = xmalloc(n + 1);
    memcpy(p, s, n + 1); /* bao gồm '\0' */
    return p;
}

/* Sao chép chuỗi giới hạn: sao chép tối đa n byte, n là kích thước-1 (không có NUL-terminate). */
static inline char *xstrndup(const char *s, size_t n) {
    if (!s) {
        char *z = xmalloc(1);
        z[0] = '\0';
        return z;
    }

    size_t m = 0;

    size_t m = strnlen(s, maxlen);
    if (m >= SIZE_MAX) {
        fprintf(stderr, "FATAL: xstrndup overflow\n");
        abort();
    }

    char *p = xmalloc(m + 1);

    if (m){
        memcpy(p, s, m);
    }

    p[m] = '\0';
    return p;
}

/* Trợ giúp sao chép nhị phân. */
static inline void *xmemcpy(const void *src, size_t n) {
    if (!src && n) {
        return NULL;
    }

    void *p = xmalloc(n ? n : 1);
    if (n){ 
        memcpy(p, src, n);
    }

    return p;
}

#endif /* SAFE_ALLOC_H */

Tại sao không sử dụng strdup trực tiếp?

  • Hàm xstrdup của chúng tôi kiểm tra tràn và xác định hành vi cho đầu vào NULL.
  • Không có bất ngờ và nhất quán với chính sách OOM của chúng tôi.

3) Các Mẫu Khởi tạo

Cấu trúc (cấu hình/ngữ cảnh)

c Copy
typedef struct {
    const char *host;
    int         port;
    int         backlog;
    void       *user_data;
} server_config_t;

/* Ưu tiên calloc cho giá trị mặc định bằng 0/NULL */
server_config_t *cfg = xcalloc(1, sizeof *cfg);
cfg->host   = xstrdup("0.0.0.0"); 
cfg->port   = 9000;
cfg->backlog= 256;

/* Hoặc: malloc + memset + khởi tạo từng trường */
server_config_t *cfg2 = xmalloc(sizeof *cfg2);
memset(cfg2, 0, sizeof *cfg2);
cfg2->port = 9000;

xfree(cfg->host);
xfree(cfg);
xfree(cfg2);

Phần đuôi của realloc phải được khởi tạo

c Copy
size_t old_cap = 16, new_cap = 64;
char *buf = xmalloc(old_cap);
/* ...ghi đến old_cap... */

buf = xrealloc(buf, new_cap);
/* Vùng mới [old_cap, new_cap) là rác → khởi tạo nếu bạn sẽ đọc nó */
memset(buf + old_cap, 0, new_cap - old_cap);

4) Các Quy ước Quyền Sở Hữu (RẤT QUAN TRỌNG)

Định nghĩa rõ ràng ai cấp phát, ai giải phóng.

  • Con trỏ vay mượn: chỉ vào bộ nhớ mà chúng tôi không giải phóng (ví dụ: chuỗi ký tự, thuộc về caller).
  • Con trỏ sở hữu: được cấp phát ở đây, phải được giải phóng trong hàm hủy.
  • Sao chép khi thiết lập: an toàn nhất—API sao chép đầu vào và quản lý nó.

Ví dụ API

c Copy
typedef struct server server_t;

server_t *server_create(void);
void      server_destroy(server_t *s);

int        server_set_host(server_t *s, const char *host); /* sao chép */
const char*server_get_host(const server_t *s);             /* vay mượn */

Triển khai:

c Copy
struct server {
    char  *host;   /* sở hữu */
    int    port;
    size_t backlog;
};

server_t *server_create(void) {
    server_t *s = xcalloc(1, sizeof(server_t));
    s->port = 9000;
    s->backlog = 256;
    return s;
}

int server_set_host(server_t *s, const char *host) {
    char *copy = xstrdup(host);  /* an toàn */
    xfree(s->host);
    s->host = copy;
    return 0;
}

const char *server_get_host(const server_t *s) {
    return s->host ? s->host : "";
}

void server_destroy(server_t *s) {
    if (!s) return;
    xfree(s->host);
    xfree(s);
}

5) Tạo/Hủy + Khởi tạo Heap

c Copy
typedef struct {
    const char *host;
    int         port;
    int         backlog;
    size_t      read_chunk;
} uvsvr_config_t;

uvsvr_config_t uvsvr_config_defaults(void) {
    return (uvsvr_config_t){
        /* NHƯ CÁC API Thư viện C thường tuyên bố nếu char *p phải được cấp phát, không phải literal*/
        .host        = xstrdup("0.0.0.0"), 
        .port        = 9000,
        .backlog     = 256,
        .read_chunk  = 16 * 1024,
    };
}

uvsvr_config_t *uvsvr_config_new(void) {
    uvsvr_config_t *p = xmalloc(sizeof *p);
    *p = uvsvr_config_defaults();
    return p;
}

6) Các Mô hình Sai (Không Làm Những Điều Này)

  • Đọc trước khi khởi tạo:
c Copy
  T *p = xmalloc(sizeof(*p));
  if (p->flag) { /* UB */ }
  • Giả định realloc sẽ xóa các byte mới.
  • Giải phóng các con trỏ vay mượn, hoặc rò rỉ các con trỏ sở hữu.
  • Sử dụng alloca cho các bộ đệm lớn/thay đổi.
  • Sử dụng các hàm chuỗi không an toàn (strcpy, strcat, strncpy cổ điển).

7) Các Đoạn Mã Thực Tiễn

Định dạng an toàn

c Copy
char buf[64];
int n = snprintf(buf, sizeof(buf), "%s", input);
if (n < 0) { /* lỗi định dạng */ }
if ((size_t)n >= sizeof(buf)) { /* bị cắt ngắn */ }

8) Danh sách Kiểm tra

  • [ ] Sử dụng xcalloc cho các cấu trúc; hoặc xmalloc + memset.
  • [ ] Luôn khởi tạo sau malloc/alloca.
  • [ ] Sau realloc, khởi tạo các byte mới.
  • [ ] Sử dụng xstrdup/xstrndup (không bao giờ tránh strdup).
  • [ ] Tập trung vào vòng đời trong create/destroy.
  • [ ] Định nghĩa quyền sở hữu trong tiêu đề.
  • [ ] xfree sau khi sử dụng → con trỏ = NULL.
  • [ ] Tránh alloca trong máy chủ sản xuất.
  • [ ] Ưu tiên snprintf/memcpy với kích thước rõ ràng.

Tóm tắt:

  • malloc/alloca = rác → phải khởi tạo.
  • calloc = giá trị mặc định bằng 0.
  • realloc = giữ lại cũ, mới là rác.
  • Sử dụng các hàm bọc an toàn (xmalloc, xcalloc, xrealloc, xstrdup, v.v.).
  • Tập trung vào cấp phát trong các API create/destroy.
  • Tài liệu hóa các hợp đồng quyền sở hữu.
  • Điều này giúp tránh UB, rò rỉ, con trỏ treo và các cạm bẫy/rủi ro cho chính bạ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