Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 14.622 phút đọc

malloc

Sau bài này bạn sẽ làm được

  • Gọi malloc đúng cách và luôn kiểm tra kết quả
  • Dùng thành ngữ sizeof của biểu thức thay vì của kiểu
  • Phát hiện tràn số khi nhân kích thước
  • Hiểu vì sao vùng vừa cấp chứa rác

Hàm malloc là cửa vào của toàn bộ thế giới cấp phát động. Nó chỉ có một tham số và một giá trị trả về, nhưng quanh nó có bốn cách viết sai mà gần như người học nào cũng mắc ít nhất một lần.

#Cách dùng

#include <stdlib.h>

void *malloc(size_t co);
ĐiểmChi tiết
Tham sốSố byte cần cấp, không phải số phần tử
Trả về khi thành côngCon trỏ tới vùng nhớ mới, căn chỉnh phù hợp với mọi kiểu
Trả về khi thất bạiNULL
Nội dung vùng nhớKhông xác định, chứa rác
Gọi với đối số 0Trả về NULL hoặc con trỏ hợp lệ không dùng được
co-ban.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    size_t n = 10;
    int   *a = malloc(n * sizeof *a);      /* xin 40 byte */

    if (a == NULL) {
        fprintf(stderr, "Khong du bo nho\n");
        return 1;
    }

    for (size_t i = 0; i < n; ++i)
        a[i] = (int)(i * i);

    for (size_t i = 0; i < n; ++i)
        printf("%d ", a[i]);

    printf("\n");

    free(a);

    return 0;
}
terminal
./co-ban
0 1 4 9 16 25 36 49 64 81

Vùng vừa cấp chứa rác

rac.c
int *a = malloc(10 * sizeof *a);

if (a == NULL) return 1;

for (size_t i = 0; i < 10; ++i)
    printf("%d ", a[i]);      /* hành vi không xác định: đọc giá trị chưa khởi tạo */
terminal
gcc -fsanitize=memory -g rac.c -o t && ./t
WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 in main rac.c:6
valgrind --track-origins=yes ./t
Conditional jump or move depends on uninitialised value(s)
 Uninitialised value was created by a heap allocation
   at malloc
   by main (rac.c:1)

#Thành ngữ sizeof của biểu thức

sizeof của kiểu
int *a = malloc(n * sizeof(int));

/* Đổi kiểu của a thành long sau này: */
long *a = malloc(n * sizeof(int));      /* chỉ cấp một nửa số byte cần */
sizeof của biểu thức trỏ tới
int *a = malloc(n * sizeof *a);

/* Đổi kiểu của a thành long: */
long *a = malloc(n * sizeof *a);        /* tự động đúng, không phải sửa gì */

Áp dụng với struct và mảng hai chiều

thanh-ngu.c
typedef struct { char ten[64]; int tuoi; double diem; } SinhVien;

/* Một struct */
SinhVien *sv = malloc(sizeof *sv);

/* Mảng n struct */
SinhVien *ds = malloc(n * sizeof *ds);

/* Mảng con trỏ */
char **ten = malloc(n * sizeof *ten);

/* Ma trận phẳng hang nhân cot */
double *m = malloc(hang * cot * sizeof *m);

/* Mảng hai chiều thật, với con trỏ tới mảng */
int (*bang)[COT] = malloc(hang * sizeof *bang);

bang[1][2] = 42;      /* dùng được cú pháp hai cặp ngoặc vuông */

#Luôn kiểm tra kết quả

kiem-tra.c
/* Mẫu tối thiểu */
int *a = malloc(n * sizeof *a);

if (a == NULL) {
    fprintf(stderr, "Khong du bo nho cho %zu phan tu\n", n);
    return 1;
}

/* Mẫu trong hàm thư viện: trả về mã lỗi, không tự in gì */
int tao_mang(size_t n, int **ra)
{
    if (ra == NULL || n == 0) return -1;

    int *a = malloc(n * sizeof *a);

    if (a == NULL) return -1;

    *ra = a;

    return 0;
}
Ba cách kiểm tra sai
/* 1. Không kiểm tra */
int *a = malloc(n * sizeof *a);
a[0] = 1;

/* 2. Kiểm tra nhưng không dừng */
int *b = malloc(n * sizeof *b);

if (b == NULL)
    printf("loi\n");      /* in rồi vẫn chạy tiếp */

b[0] = 1;

/* 3. Kiểm tra sau khi đã dùng */
int *c = malloc(n * sizeof *c);

c[0] = 1;

if (c == NULL) return 1;
Cách đúng
int *a = malloc(n * sizeof *a);

if (a == NULL) {
    fprintf(stderr, "Khong du bo nho\n");
    return 1;
}

a[0] = 1;

#Bốn lỗi kinh điển

Lỗi 1: quên nhân với sizeof

int *a = malloc(n);                /* SAI: chỉ cấp n byte, cần 4n */
int *b = malloc(n * sizeof *b);    /* đúng */

Lỗi 2: nhân sizeof hai lần

int *a = malloc(n * sizeof *a);

a = a + 10 * sizeof *a;      /* SAI: số học con trỏ đã nhân sizeof rồi */
a = a + 10;                  /* đúng */

Bài 12.8 đã nói về chuyện này. Trong ngữ cảnh cấp phát, hai lỗi này hay đi cùng nhau: người viết lúc thì quên sizeof, lúc thì thừa.

Lỗi 3: dùng sizeof của con trỏ

int *a = malloc(n * sizeof a);      /* SAI: sizeof a là 8, kích thước con trỏ */
int *b = malloc(n * sizeof *b);     /* đúng: sizeof *b là 4 */

Lỗi 4: ép kiểu che mất lỗi thiếu file tiêu đề

thieu-header.c
/* Quên #include <stdlib.h> */

int main(void)
{
    int *a = (int *)malloc(40);      /* ép kiểu che mất cảnh báo */

    return 0;
}
terminal
gcc -std=c17 -Wall thieu-header.c -o t
thieu-header.c:5:22: error: implicit declaration of function 'malloc' [-Wimplicit-function-declaration]

#Tràn số khi nhân kích thước

tran.c
size_t n;

if (scanf("%zu", &n) != 1) return 1;

int *a = malloc(n * sizeof *a);      /* nếu n rất lớn, phép nhân TRÀN */

if (a == NULL) return 1;

for (size_t i = 0; i < n; ++i)
    a[i] = 0;                        /* ghi ra ngoài vùng đã cấp */
Nhân không kiểm tra
int *a = malloc(n * sizeof *a);
Ba cách kiểm tra
/* Cách 1: kiểm tra trước khi nhân */
if (n > SIZE_MAX / sizeof *a) {
    fprintf(stderr, "So phan tu qua lon\n");
    return 1;
}

int *a = malloc(n * sizeof *a);

/* Cách 2: dùng calloc, nó tự kiểm tra tràn */
int *b = calloc(n, sizeof *b);

/* Cách 3: dùng hàm sẵn có của GCC và Clang */
size_t tong;

if (__builtin_mul_overflow(n, sizeof *a, &tong)) {
    fprintf(stderr, "Tran so\n");
    return 1;
}

int *c = malloc(tong);
an-toan.c
#include <stdint.h>
#include <stdlib.h>

/* Cấp phát mảng n phần tử kích thước co, có kiểm tra tràn.
   Trả về NULL nếu tràn hoặc không đủ bộ nhớ. */
void *cap_mang(size_t n, size_t co)
{
    if (n == 0 || co == 0)      return NULL;
    if (n > SIZE_MAX / co)      return NULL;      /* sẽ tràn */

    return malloc(n * co);
}

int *a = cap_mang(n, sizeof *a);
terminal
# Trình dò lỗi bắt được ngay nếu bạn ghi ra ngoài
gcc -fsanitize=address -g tran.c -o t && echo 4611686018427387905 | ./t
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014
WRITE of size 4
0x602000000014 is located 0 bytes to the right of 4-byte region

Tự làm thử

  1. Cấp phát mảng mười số nguyên, điền giá trị, in ra rồi giải phóng. Chạy dưới valgrind để xác nhận không rò rỉ.
  2. In nội dung một vùng vừa cấp phát mà chưa khởi tạo, chạy dưới valgrind --track-origins=yes.
  3. Viết malloc(n * sizeof a) thay vì sizeof *a với kiểu char, đo bộ nhớ thật sự chiếm.
  4. Cấp phát ma trận động bằng cú pháp int (*bang)[COT], xác nhận chỉ cần một lần free.
  5. Cài chương trình đọc n từ bàn phím rồi cấp phát, nhập một số rất lớn để gây tràn, chạy dưới -fsanitize=address.
  6. Cài hàm cap_mang có kiểm tra tràn, kiểm chứng nó trả về NULL đúng lúc.
  7. Viết hàm bọc kiểu cấp hoặc chết, giải thích vì sao không dùng được trong thư viện.

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

  • Luôn viết malloc(n * sizeof *p), không viết sizeof(kiểu) và tuyệt đối không viết sizeof p.
  • Luôn kiểm tra kết quả khác NULL ngay lập tức, và dừng chứ không chỉ in thông báo.
  • Vùng vừa cấp chứa rác. Nếu nó tình cờ toàn 0 ở lần thử đầu thì đó là may mắn chứ không phải bảo đảm.
  • Kiểm tra tràn trước khi nhân bằng n > SIZE_MAX / co, hoặc dùng calloc vốn tự kiểm tra.
  • Trong C không cần ép kiểu kết quả của malloc, vì void * tự chuyển sang mọi kiểu con trỏ đối tượng.