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ỉ.
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
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ớ
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, …) mà 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ờ.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
malloc trả về NULL trong trường hợp nào, và vì sao phải kiểm tra?
- 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ỏpcò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 khungham→ không aifreeđược → rò rỉ. - 2
Mảng động
Viết đoạn cấp phát một mảng
nsốintlúc chạy (nđọc từ người dùng), gán giá trị, rồifree.Hoàn thành khi:
int *a = malloc(n * sizeof(int));…dùnga[i]…free(a); - 3
calloc
int *a = calloc(4, sizeof(int));thìa[0]bằng bao nhiêu? Vì sao khácmalloc?Hoàn thành khi: 0 -
callocxoá trắng toàn bộ về 0;mallocthì để rác. - 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ả địnhacũ vẫn đúng?Hoàn thành khi:
realloccó thể dời khối sang vùng khác; con trỏacũ 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ếurealloctrảNULL. - 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;
freelầ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
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), đặtp = NULL;. Khi đó lỡ dùng*psẽ sập rõ ràng, vàfree(NULL)là hợp lệ (không làm gì).