Mọi mảng bạn viết cho đến giờ đều có kích thước cố định từ lúc biên dịch: int scores[10]; dành sẵn mười ô dù bạn cần ba hay ba trăm. Cách đó ổn cho đến khi kích thước chỉ biết được lúc chương trình chạy, như một tệp có bao nhiêu dòng, người dùng nhập bao nhiêu bản ghi. Bộ nhớ động (dynamic memory) giải quyết điều đó: bạn xin đúng số byte cần, vào lúc cần, và trả lại khi dùng xong.
Các công cụ nằm trong <stdlib.h>:
#include <stdlib.h>
Vì sao có heap
C cho chương trình của bạn hai vùng bộ nhớ chính. Biến cục bộ nằm trong vùng lưu trữ tự động (stack): trình biên dịch quyết định kích thước của chúng, và chúng biến mất ngay khi hàm trả về. Các vùng cấp phát động nằm trong heap: bạn quyết định kích thước lúc chạy, và khối bộ nhớ tồn tại cho đến khi bạn giải phóng nó một cách tường minh.
Tính chất thứ hai đó là lý do thật sự để dùng malloc. Một hàm có thể cấp phát một khối, trả về con trỏ, và bộ nhớ vẫn hợp lệ ở nơi gọi, điều mà một mảng cục bộ không bao giờ làm được. Hai vùng này được so sánh chi tiết trong bài stack và heap.
malloc: xin cấp byte
malloc nhận số byte và trả về một void * trỏ tới chừng đó byte bộ nhớ chưa khởi tạo, hoặc NULL nếu yêu cầu thất bại.
Bốn điểm trong chương trình ngắn đó là toàn bộ nguyên tắc:
- Biểu thức kích thước là
n * sizeof *scores. Hãy đọc là "n phần tử thuộc kiểu màscorestrỏ tới". - Kết quả được kiểm tra với
NULLtrước mọi lần dùng. - Khối bộ nhớ được dùng y như một mảng:
scores[i]chạy được vì truy cập chỉ số chính là số học con trỏ. - Nó được giải phóng một lần, sau đó con trỏ được gán
NULL.
Để ý là không có ép kiểu trên giá trị trả về. Trong C, void * tự động chuyển sang mọi con trỏ đối tượng, và viết (int *)malloc(...) chỉ thêm rối đồng thời che giấu việc thiếu #include <stdlib.h>. (C++ bắt buộc ép kiểu; C thì không.)
Mẫu dùng sizeof
Vì sao dùng sizeof *scores thay vì sizeof(int)? Vì nó không thể bị lỗi thời.
int *a = malloc(n * sizeof *a); /* n ints, whatever int is here */
long *b = malloc(n * sizeof *b); /* same line shape, right size */
/* the fragile form */
long *c = malloc(n * sizeof(int)); /* compiles, allocates too little */
Dòng cuối là một bug thật mà không trình biên dịch nào cảnh báo: c là long *, vùng cấp phát lại tính theo int, và mọi lần ghi vượt quá nửa đầu đều tràn ra ngoài. Với sizeof *c, kích thước tự động theo khai báo.
sizeof *p không giải tham chiếu p: sizeof được tính lúc biên dịch chỉ dựa trên kiểu, nên an toàn ngay cả khi p chưa khởi tạo hoặc là NULL.
Bộ nhớ chưa khởi tạo
malloc không xóa sạch vùng nhớ nó đưa cho bạn. Các byte vẫn chứa bất cứ thứ gì có ở đó từ trước.
Khi bạn muốn khối bộ nhớ được đặt về 0 sẵn, calloc làm điều đó trong một bước, xem bài calloc và realloc.
free: trả lại bộ nhớ
free(p) trả khối bộ nhớ về cho bộ cấp phát. Ba quy tắc:
- Truyền đúng con trỏ mà
malloctrả về, không phải con trỏ đã bị dịch đi.free(p + 1)là undefined behavior. - Giải phóng mỗi khối đúng một lần. Hai lần là double free, làm hỏng chính sổ sách nội bộ của bộ cấp phát.
- Sau khi giải phóng, con trỏ đã cũ. Dùng nó là use-after-free.
int *p = malloc(sizeof *p);
free(p);
*p = 5; /* use-after-free - undefined behavior */
free(p); /* double free - undefined behavior */
Không dòng nào trong đó bắt buộc phải sập ngay, và đó chính là điều khiến chúng nguy hiểm: chương trình có thể chạy vài phút rồi hỏng ở một chỗ chẳng liên quan. Cách phòng vệ rẻ là xóa trắng con trỏ:
free(p);
p = NULL;
free(NULL) được định nghĩa rõ là không làm gì, nên một lần free(p) thứ hai sau đó là vô hại, và *p trở thành một lần sập ngay tại đúng chỗ lỗi thay vì làm hỏng dữ liệu âm thầm.
Một mảng động từ đầu đến cuối
Đây là hình dạng của một lần cấp phát thực tế: đọc số lượng lúc chạy, tính kích thước khối từ đó, dùng nó, giải phóng nó.
Việc ép số lượng sang size_t trở nên quan trọng khi kích thước lớn: n * sizeof *values với n là int thường có thể tràn số trước khi phép nhân kịp tới malloc, tạo ra một khối nhỏ hơn nhiều so với dự định. Nhân trong size_t tránh được điều đó.
Cấp phát bên trong hàm
Quy tắc vòng đời của heap là điều khiến việc này hợp lệ, và cũng là điều khiến việc trả về một mảng cục bộ không hợp lệ:
len + 1 chừa chỗ cho ký tự kết thúc null, cùng quy tắc tính kích thước như mọi nơi khác với chuỗi. Và để ý chú thích phía trên hàm: khi một hàm trả về bộ nhớ đã cấp phát, ai giải phóng nó là một phần trong cam kết của hàm. Ghi điều đó ra không phải thủ tục giấy tờ; đó là thứ duy nhất ngăn rò rỉ bộ nhớ.
Struct trên heap
Cùng mẫu đó dùng được cho mọi kiểu:
Để ý nhánh lỗi thứ hai giải phóng p trước khi trả về. Khi một hàm giữ nhiều hơn một vùng cấp phát, mọi đường xử lý lỗi đều phải giải phóng những vùng đã lấy, đây là nguồn phổ biến nhất của các vụ rò rỉ được mô tả trong bài rò rỉ bộ nhớ.
Danh sách kiểm tra
- Luôn kiểm tra
mallocvớiNULLtrước lần dùng đầu tiên. - Tính kích thước bằng
n * sizeof *p, nhân trongsize_t. - Không ép kiểu giá trị trả về trong C.
- Bộ nhớ từ
mallocchưa được khởi tạo; hãy ghi trước khi đọc. - Giải phóng đúng một lần, bằng con trỏ gốc, rồi gán nó bằng
NULL. - Mọi đường xử lý lỗi sau một lần cấp phát phải giải phóng những gì đã giữ.
Câu hỏi thường gặp
malloc trong C dùng để làm gì?
malloc(n) xin n byte bộ nhớ từ heap và trả về con trỏ tới đầu khối đó, hoặc NULL nếu không đáp ứng được yêu cầu. Khối bộ nhớ tồn tại cho đến khi bạn truyền chính con trỏ đó cho free: khác với biến cục bộ, nó không được giải phóng khi hàm trả về.
Dùng malloc và free trong C như thế nào?
Cấp phát bằng int *p = malloc(n * sizeof *p);, kiểm tra if (p == NULL) trước khi dùng, dùng nó như một mảng, rồi gọi free(p); đúng một lần khi xong việc. Gán p = NULL sau đó sẽ biến một lần vô tình dùng lại về sau thành một lần sập rõ ràng thay vì làm hỏng dữ liệu âm thầm.
Vì sao malloc(n * sizeof *p) tốt hơn malloc(n * sizeof(int))?
Vì sizeof *p tự động theo kiểu của con trỏ. Nếu sau này p trở thành long * hoặc struct Point *, kích thước cấp phát tự cập nhật; viết cứng sizeof(int) sẽ để lại một kích thước giờ đã sai mà trình biên dịch không hề báo.
Chuyện gì xảy ra nếu không free bộ nhớ trong C?
Khối bộ nhớ vẫn được cấp phát suốt vòng đời của tiến trình, đó là rò rỉ bộ nhớ. Một chương trình ngắn không sao vì hệ điều hành thu hồi mọi thứ khi thoát, nhưng một chương trình chạy lâu sẽ rò rỉ một ít ở mỗi lần lặp và cuối cùng cạn bộ nhớ. Xem rò rỉ bộ nhớ.