← Lập trình C nâng cao

Bài 7 · Nâng cao · 26 phút· Cập nhật 11/06/2026

Cấp phát động (malloc/free)

Biên soạn bởi Nguyễn Anh Tuấn

Cấp phát bộ nhớ động trong C (dynamic allocation): malloc/calloc/realloc/free, heap vs stack, rò rỉ bộ nhớ (memory leak), use-after-free và double free.

Biến cục bộ trên stack có hai giới hạn: biến mất khi hàm trả về, và phải biết kích thước lúc biên dịch. Khi cần bộ nhớ có kích thước quyết định lúc chạy hoặc sống lâu hơn một hàm, ta xin trên heap bằng malloc:

mang-dong.c

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

int main() {
    int n = 3;
    int *arr = malloc(n * sizeof(int));  // mang n phan tu luc chay
    arr[0] = 10; arr[1] = 20; arr[2] = 30;
    printf("%d\n", arr[1]);   // 20
    free(arr);                 // tra lai khi xong
    return 0;
}

Kết quả khi chạy

20
  • malloc(số_byte) → con trỏ tới khối trên heap (hoặc NULL nếu hết bộ nhớ).
  • Tính kích thước bằng sizeof: n * sizeof(int), không hardcode số byte.
  • Khối heap sống tới khi free - không tự biến mất khi hàm trả về.

Chu trình chuẩn: xin → kiểm tra NULL → dùng → free → đặt NULL. Chọn các ví dụ bên dưới (đúng cách, rồi ba lỗi kinh điển) và bấm Bước tiếp để xem heap thay đổi - ô đỏ báo bước có lỗi:

malloc → kiểm tra NULL → dùng → free → đặt NULL. Sạch sẽ, không rò rỉ.

Bước 1/7
chuong-trinh.c
▲ địa chỉ cao
Stack khung hàm · biến cục bộ · tham số
- stack trống -
↓ lớn dần xuống↑ lớn dần lên
- heap trống -
Heap cấp phát động (malloc / free)
Data segment
.bss - global/static chưa khởi tạo (mặc định 0)
-
.data - global/static đã khởi tạo
-
Text segment
.text - mã lệnh & dữ liệu chỉ đọc
mã: main()chỉ đọc
▼ địa chỉ thấp
// chương trình được nạp - Mã ở .text. Chưa có biến nào.

dung-cach.c

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

int main() {
    int *p = malloc(sizeof(int));
    if (p == NULL) return 1;   // luon kiem tra
    *p = 42;
    printf("%d\n", *p);        // 42
    free(p);                   // tra lai
    p = NULL;                  // tranh con tro treo
    return 0;
}

Kết quả khi chạy

42

Hai họ hàng của malloc. calloc cấp phát VÀ xoá trắng về 0; realloc đổi kích thước một khối đã cấp (giữ dữ liệu cũ):

calloc-realloc.c

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

int main() {
    int *a = calloc(2, sizeof(int)); // 2 phan tu, TAT CA = 0
    printf("%d\n", a[0]);            // 0  (calloc xoa trang)

    int *tmp = realloc(a, 4 * sizeof(int)); // mo rong len 4 phan tu
    if (tmp == NULL) { free(a); return 1; }    // that bai: khoi cu VAN con
    a = tmp;
    a[2] = 30;                       // giu a[0], a[1] cu; them cho moi
    printf("%d %d\n", a[0], a[2]);   // 0 30
    free(a);
    return 0;
}

Kết quả khi chạy

0
0 30
  • calloc(n, size): cấp n×size byte và đặt tất cả về 0.
  • realloc(p, size_mới): đổi kích thước, GIỮ dữ liệu cũ; có thể dời khối.
  • Nhận kết quả realloc vào con trỏ TẠM (tmp) rồi mới gán - tránh mất khối cũ khi realloc trả NULL.

Phần lớn lỗi nghiêm trọng trong C đến từ heap. Vừa đọc vừa mở lại các ví dụ ở Bước 2 để đối chiếu:

  • Rò rỉ (leak): malloc nhưng quên free (hoặc mất con trỏ) → khối kẹt mãi, ngốn RAM.
  • Dùng sau free (use-after-free): truy cập qua con trỏ đã free → hành vi không xác định.
  • Free hai lần (double free): free cùng một khối hai lần → hỏng cấu trúc quản lý heap.

Vì sao nguy hiểm hơn lỗi stack

Lỗi heap thường không sập ngay mà làm hỏng dữ liệu âm thầm, đến tận nơi khác mới lộ ra - rất khó lần. Use-after-free và double-free còn là nguồn của nhiều lỗ hổng bảo mật nghiêm trọng. Công cụ như valgrind hoặc AddressSanitizer giúp phát hiện chúng.

Vài quy tắc đơn giản loại bỏ phần lớn lỗi heap:

  • Mỗi malloc/calloc ↔ đúng MỘT free. Nghĩ ngay tới free khi viết malloc.
  • Luôn kiểm tra kết quả malloc khác NULL trước khi dùng.
  • Sau free(p), đặt p = NULL (free(NULL) vô hại; tránh dùng lại con trỏ treo).
  • Mỗi khối có MỘT “chủ sở hữu” rõ ràng chịu trách nhiệm free.
  • realloc: luôn gán lại con trỏ bằng giá trị trả về.

Liên hệ bố cục bộ nhớ

Heap là vùng bạn tự quản (xem bài Bố cục bộ nhớ): lớn dần lên, sống tới khi free. Stack thì tự động - đó là lý do biến cục bộ “miễn phí” nhưng không sống lâu được. Chọn đúng vùng cho đúng nhu cầu.

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

Một con trỏ tới khối byte liên tục trên HEAP (kiểu void*, tự gán vào con trỏ bất kỳ). Nếu hết bộ nhớ, malloc trả về NULL - vì thế phải kiểm tra trước khi dùng. malloc KHÔNG khởi tạo nội dung (chứa rác).

Heap không tự dọn. Mỗi khối malloc tồn tại tới khi bạn free (hoặc chương trình kết thúc). Quên free = rò rỉ bộ nhớ: với chương trình chạy lâu (server, game) sẽ ngốn dần hết RAM rồi sập.

malloc(size) cấp size byte nhưng KHÔNG khởi tạo (rác). calloc(n, size) cấp n×size byte VÀ xoá trắng toàn bộ về 0. Dùng calloc khi cần vùng nhớ bắt đầu từ 0 (vd mảng đếm).

realloc(p, size_moi) đổi kích thước khối đã cấp, giữ nguyên dữ liệu cũ trong phạm vi chung. Nó CÓ THỂ phải dời khối sang vùng mới (nếu chỗ cũ không đủ) → trả về địa chỉ mới. Vì thế nhận kết quả vào con trỏ TẠM: int *tmp = realloc(p, …); if (tmp) p = tmp; - nếu viết thẳng p = realloc(p, …)realloc thất bại (trả NULL), khối cũ mất dấu và rò rỉ vĩnh viễn.

Con trỏ trỏ tới vùng nhớ KHÔNG còn hợp lệ: đã free, hoặc là biến cục bộ của hàm đã kết thúc. Dùng nó (đọc/ghi) là use-after-free - hành vi không xác định: có thể “chạy được” một lúc rồi hỏng bất ngờ.

Bài Bẫy & quản lý bộ nhớ →

Không, và KHÔNG nên trong C: void* tự chuyển sang con trỏ đích. (int *p = malloc(4); là đủ.) Ép kiểu thừa còn có thể che một lỗi quên #include <stdlib.h>. Lưu ý: trong C++ thì lại bắt buộc ép.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

malloc trả về NULL trong trường hợp nào, và vì sao phải kiểm tra?

  1. 1

    Theo dõi rò rỉ

    Trong mô phỏng Bước 2, chọn ví dụ “Rò rỉ”. Sau khi ham() kết thúc, khối heap còn không? con trỏ p còn không? Vì sao đó là rò rỉ?

    Hoàn thành khi: Khối VẪN còn trên heap; con trỏ p (duy nhất trỏ tới khối) biến mất theo khung ham → không ai free được → rò rỉ.

  2. 2

    Mảng động

    Viết đoạn cấp phát một mảng n số int lúc chạy (n đọc từ người dùng), gán giá trị, rồi free.

    Hoàn thành khi: int *a = malloc(n * sizeof(int)); …dùng a[i]free(a);

  3. 3

    calloc

    int *a = calloc(4, sizeof(int)); thì a[0] bằng bao nhiêu? Vì sao khác malloc?

    Hoàn thành khi: 0 - calloc xoá trắng toàn bộ về 0; malloc thì để rác.

  4. 4

    realloc dời khối

    Sau a = realloc(a, n2 * sizeof(int)); vì sao phải dùng con trỏ TRẢ VỀ, không giả định a cũ vẫn đúng?

    Hoàn thành khi: realloc có thể dời khối sang vùng khác; con trỏ a cũ khi đó thành treo. Gán lại bằng giá trị trả về - qua con trỏ tạm, để không mất khối cũ nếu realloc trả NULL.

  5. 5

    Double free

    Mô phỏng Bước 2, chọn “Free hai lần”. Vì sao free(p) lần hai là lỗi?

    Hoàn thành khi: Khối đã được trả về hệ thống rồi; free lần hai làm hỏng cấu trúc quản lý heap của allocator → thường sập hoặc thành lỗ hổng bảo mật.

  6. 6

    Tránh con trỏ treo

    Thói quen đơn giản nào giúp giảm cả use-after-free lẫn double-free?

    Hoàn thành khi: Sau free(p), đặt p = NULL;. Khi đó lỡ dùng *p sẽ sập rõ ràng, và free(NULL) là hợp lệ (không làm gì).