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ểm | Chi tiết |
|---|---|
| Tham số | Số byte cần cấp, không phải số phần tử |
| Trả về khi thành công | Con 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ại | NULL |
| Nội dung vùng nhớ | Không xác định, chứa rác |
| Gọi với đối số 0 | Trả 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:6valgrind --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ử
- 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ỉ.
- 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. - Viết
malloc(n * sizeof a)thay vìsizeof *avới kiểuchar, đo bộ nhớ thật sự chiếm. - 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ầnfree. - Cài chương trình đọc
ntừ 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. - Cài hàm
cap_mangcó kiểm tra tràn, kiểm chứng nó trả vềNULLđúng lúc. - 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ếtsizeof(kiểu)và tuyệt đối không viếtsizeof 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ùngcallocvố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.