Bỏ qua điều hướng, tới nội dung chính
Học C
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ỏiTrả lời
Vì sao không cần truyền kích thướcBộ 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ôngThườ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ôngKhông. Dữ liệu vẫn nằm đó cho tới khi bị ghi đè
Con trỏ có bị đổi khôngKhông. Nó vẫn giữ nguyên địa chỉ cũ, và trở thành con trỏ treo
Gọi với NULL thì saoHoà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 íchGiải quyết được lỗi nào
Giải phóng hai lần trở thành vô hạifree(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 ifCon 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ử

  1. Gọi free lê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.
  2. 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.
  3. Cài hàm xoa_sach dùng volatile, kiểm chứng nó không bị trình biên dịch bỏ với -O2.
  4. Chạy chương trình với MALLOC_PERTURB_=165 và quan sát giá trị trong vùng vừa cấp phát.
  5. 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.
  6. 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.
  7. Viết macro GIAI_PHONG không có do while (0), dùng nó trong if else khô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 free con trỏ gốc từ malloc, calloc, realloc, hoặc NULL.
  • free(NULL) luôn hợp lệ, nên hàm dọn dẹp không cần kiểm tra trước.
  • free khô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 NULL sau free biế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 đó.