Bài 14.918 phút đọc
free
Sau bài này bạn sẽ làm được
- Biết chính xác con trỏ nào được phép truyền cho free
- Hiểu vì sao free không cần biết kích thước
- Đặt NULL sau khi giải phóng một cách nhất quán
- Giải phóng đúng thứ tự với cấu trúc lồng nhau
Hàm free chỉ có một tham số và không trả về gì, nhưng nó là nơi bốn loại lỗi bộ nhớ ở bốn bài cuối chương này bắt nguồn. Bài này nói rõ con trỏ nào được phép truyền cho nó, và chuyện gì thật sự xảy ra bên trong.
#Cách dùng
#include <stdlib.h>
void free(void *p);co-ban.c
int *a = malloc(10 * sizeof *a);
if (a == NULL) return 1;
/* ... dùng a ... */
free(a);
a = NULL; /* không bắt buộc theo chuẩn, nhưng nên làm */| Câu hỏi | Trả lời |
|---|---|
| Vì sao không cần truyền kích thước | Bộ cấp phát lưu kích thước trong phần đầu ẩn ngay trước vùng của bạn, như Bài 14.2 đã vẽ |
| Bộ nhớ có trả về hệ điều hành không | Thường là không. Nó được đánh dấu trống và tái dùng cho lần malloc sau |
| Nội dung có bị xóa không | Không. Dữ liệu vẫn nằm đó cho tới khi bị ghi đè |
| Con trỏ có bị đổi không | Không. Nó vẫn giữ nguyên địa chỉ cũ, và trở thành con trỏ treo |
| Gọi với NULL thì sao | Hoàn toàn hợp lệ, hàm không làm gì cả |
#Bốn quy tắc bắt buộc
Quy tắc chung
Chỉ được truyền cho
free một con trỏ do malloc, calloc hoặc realloc trả về, và chưa từng được giải phóng. Hoặc NULL.Quy tắc 1: không giải phóng con trỏ không phải từ cấp phát động
int x = 42;
int a[10];
int *p = &x;
char *s = "abc";
free(p); /* SAI: x nằm trên ngăn xếp */
free(a); /* SAI: a nằm trên ngăn xếp */
free(s); /* SAI: hằng chuỗi nằm ở .rodata */
free((void *)main);/* SAI: hàm nằm ở .text */terminal
./sai
free(): invalid pointer Aborted (core dumped)
Quy tắc 2: không giải phóng con trỏ vào giữa khối
Giải phóng con trỏ đã bị dịch
int *a = malloc(10 * sizeof *a);
a += 5; /* con trỏ đã dịch đi 5 phần tử */
free(a); /* SAI: đây không phải địa chỉ bộ cấp phát đã trả về */Giữ con trỏ gốc
int *a = malloc(10 * sizeof *a);
if (a == NULL) return 1;
int *p = a; /* dùng con trỏ phụ để duyệt */
for (size_t i = 0; i < 10; ++i)
*p++ = (int)i;
free(a); /* giải phóng con trỏ GỐC */Quy tắc 3: không giải phóng hai lần
int *a = malloc(40);
free(a);
free(a); /* SAI: hành vi không xác định, là lỗ hổng bảo mật */Bài 14.12 dành riêng cho lỗi này, vì cơ chế phía sau nó đáng hiểu kỹ.
Quy tắc 4: không dùng sau khi giải phóng
int *a = malloc(40);
free(a);
a[0] = 1; /* SAI: ghi vào vùng đã trả lại */
printf("%d\n", a[0]); /* SAI: đọc cũng vậy */Bài 14.13 dành riêng cho lỗi này.
#free không xóa dữ liệu
con-nguyen.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *mat_khau = malloc(32);
if (mat_khau == NULL) return 1;
strcpy(mat_khau, "bi-mat-rat-quan-trong");
free(mat_khau);
/* Cấp lại cùng kích thước, rất có thể nhận được đúng khối vừa trả */
char *moi = malloc(32);
if (moi == NULL) return 1;
printf("[%s]\n", moi); /* rất có thể in ra mật khẩu cũ */
free(moi);
return 0;
}terminal
./con-nguyen
[bi-mat-rat-quan-trong]
Chế độ gỡ lỗi điền giá trị nhận biết
terminal
# glibc có thể điền giá trị vào vùng vừa giải phóng
MALLOC_PERTURB_=165 ./chuong-trinh
[\xa5\xa5\xa5\xa5...] # 165 là 0xa5, dễ nhận ra khi gỡ lỗi
# Trình dò lỗi địa chỉ giữ vùng đã giải phóng trong khu cách ly
gcc -fsanitize=address -g bai.c -o t && ./t
ERROR: AddressSanitizer: heap-use-after-free
#Giải phóng cấu trúc lồng nhau
long-nhau.c
#include <stdlib.h>
#include <string.h>
typedef struct {
char *ten; /* cấp phát riêng */
int *diem; /* cấp phát riêng */
size_t n_diem;
} SinhVien;
typedef struct {
SinhVien *sv; /* mảng cấp phát riêng */
size_t n;
} Lop;
/* Giải phóng từ TRONG ra NGOÀI */
void sv_huy(SinhVien *sv)
{
if (sv == NULL) return;
free(sv->ten);
free(sv->diem);
sv->ten = NULL;
sv->diem = NULL;
}
void lop_huy(Lop *l)
{
if (l == NULL) return;
for (size_t i = 0; i < l->n; ++i)
sv_huy(&l->sv[i]); /* giải phóng từng sinh viên trước */
free(l->sv); /* rồi mới giải phóng mảng */
l->sv = NULL;
l->n = 0;
}Giải phóng từ ngoài vào trong
free(l->sv); /* mảng đã đi rồi */
for (size_t i = 0; i < l->n; ++i)
free(l->sv[i].ten); /* đọc vùng đã giải phóng */Từ trong ra ngoài
for (size_t i = 0; i < l->n; ++i) {
free(l->sv[i].ten);
free(l->sv[i].diem);
}
free(l->sv);Cấp phát một lần cho cả struct và dữ liệu
mot-lan.c
/* Kỹ thuật mảng linh hoạt của C99: đặt mảng độ dài chưa xác định ở CUỐI struct. */
typedef struct {
size_t n;
int du_lieu[]; /* mảng thành viên linh hoạt */
} Mang;
Mang *mang_tao(size_t n)
{
if (n > (SIZE_MAX - sizeof(Mang)) / sizeof(int)) return NULL;
Mang *m = malloc(sizeof *m + n * sizeof *m->du_lieu);
if (m == NULL) return NULL;
m->n = n;
return m;
}
Mang *m = mang_tao(100);
if (m != NULL) {
m->du_lieu[0] = 42;
free(m); /* MỘT lần free duy nhất */
}#Đặt NULL sau khi giải phóng
free(a);
a = NULL;| Lợi ích | Giải quyết được lỗi nào |
|---|---|
| Giải phóng hai lần trở thành vô hại | free(NULL) hợp lệ, nên gọi lần hai không sao |
| Dùng sau khi giải phóng sập ngay tại chỗ | Thay vì đọc dữ liệu cũ và chạy tiếp với kết quả sai |
| Kiểm tra được bằng if | Con trỏ treo không kiểm tra được, con trỏ rỗng thì có |
dat-null.c
int *a = malloc(40);
free(a);
/* KHÔNG đặt NULL */
free(a); /* hành vi không xác định, có thể sập, có thể phá cấu trúc */
a[0] = 1; /* âm thầm ghi vào vùng người khác */
/* CÓ đặt NULL */
a = NULL;
free(a); /* hợp lệ, không làm gì */
a[0] = 1; /* sập NGAY tại đây, thông báo rõ ràng */Gói lại thành macro
/* Cặp do while không có thân là thành ngữ chuẩn để macro dùng được
như một câu lệnh, kể cả sau if không có ngoặc nhọn. */
#define GIAI_PHONG(p) do { free(p); (p) = NULL; } while (0)
int *a = malloc(40);
GIAI_PHONG(a); /* a giờ là NULL */
if (co_loi)
GIAI_PHONG(a); /* hoạt động đúng kể cả không có ngoặc nhọn */
else
dung_tiep(a);Tự làm thử
- Gọi
freelên con trỏ tới biến cục bộ, lên hằng chuỗi và lên con trỏ đã bị dịch. Chép lại ba thông báo lỗi. - Cấp phát, ghi mật khẩu vào, giải phóng, rồi cấp lại cùng kích thước và in nội dung.
- Cài hàm
xoa_sachdùngvolatile, kiểm chứng nó không bị trình biên dịch bỏ với-O2. - Chạy chương trình với
MALLOC_PERTURB_=165và quan sát giá trị trong vùng vừa cấp phát. - Cài cấu trúc lồng ba tầng, viết hàm hủy đúng thứ tự và chạy dưới valgrind.
- Cài struct dùng mảng thành viên linh hoạt, xác nhận chỉ cần một lần
free. - Viết macro
GIAI_PHONGkhông códo while (0), dùng nó trongif elsekhông ngoặc nhọn và đọc thông báo lỗi.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Chỉ truyền cho
freecon trỏ gốc từmalloc,calloc,realloc, hoặcNULL. free(NULL)luôn hợp lệ, nên hàm dọn dẹp không cần kiểm tra trước.freekhông xóa dữ liệu. Với dữ liệu nhạy cảm, phải chủ động xóa bằng cách trình biên dịch không được phép tối ưu bỏ.- Giải phóng cấu trúc lồng nhau từ trong ra ngoài, đúng ngược thứ tự tạo.
- Đặt
NULLsaufreebiến hai loại lỗi thành vô hại, nhưng chỉ bảo vệ đúng biến đó chứ không bảo vệ các con trỏ khác cùng trỏ vào vùng đó.