malloc trả lời một câu hỏi: cho tôi chừng này byte. Hai hàm đi cùng trong <stdlib.h> trả lời những câu hỏi tiếp theo: cho tôi chừng này byte, đã được xóa sạch (calloc), và tôi cần khối nhớ đang có lớn hơn (realloc).
calloc: số phần tử, kích thước và các số 0
void *calloc(size_t count, size_t size);
Hai đối số thay vì một, và kết quả được ghi đầy các byte 0.
Đây là trường hợp kinh điển: một mảng histogram hay mảng đếm mà mọi ô phải bắt đầu từ 0. Với malloc bạn cần một vòng lặp; calloc làm việc đó ngay trong lúc cấp phát, thường là không tốn thêm gì vì hệ điều hành vốn đã cấp các trang nhớ mới ở trạng thái toàn số 0.
Lý do về tràn số
Việc tách thành hai đối số không chỉ để cho đẹp. Thử xét một số lượng được đọc từ tệp:
size_t n = huge_value_from_input;
int *a = malloc(n * sizeof *a); /* the product can wrap around */
int *b = calloc(n, sizeof *b); /* required to detect the overflow */
Nếu n * sizeof *a vượt quá giá trị mà size_t chứa được, nó quay vòng thành một số nhỏ, malloc thành công với một khối bé xíu, và mọi lần ghi sau đó đều chạy xa quá cuối khối. Tiêu chuẩn C yêu cầu calloc phải thất bại và trả về NULL trong trường hợp này. Khi kích thước đến từ bên ngoài chương trình, phép kiểm tra đó rất đáng có.
"Đặt về 0" thật ra nghĩa là gì
calloc ghi các byte 0. Với số nguyên và ký tự, đó chính là giá trị 0, đúng thứ bạn cần. Với con trỏ và số thực, toàn bộ bit bằng 0 là con trỏ null và 0.0 trên mọi nền tảng phổ biến, nhưng tiêu chuẩn C không hứa điều đó. Code cần tính di động tuyệt đối sẽ gán rõ ràng các giá trị này.
calloc so với malloc + memset
int *a = calloc(n, sizeof *a);
int *b = malloc(n * sizeof *b);
if (b != NULL) {
memset(b, 0, n * sizeof *b);
}
Hai cách cho ra cùng một kết quả. Nên ưu tiên calloc: chỉ một dòng, có kiểm tra tràn số, và với các khối lớn nó có thể không cần chạm vào vùng nhớ chút nào. Hãy dùng malloc thường khi bạn sắp ghi đè mọi byte: đặt về 0 cả một megabyte mà bạn sẽ ghi đầy ngay sau đó là lãng phí hoàn toàn.
realloc: thay đổi kích thước
void *realloc(void *p, size_t newsize);
realloc trả về một khối newsize byte có nội dung giống khối cũ trong phạm vi kích thước nhỏ hơn giữa hai khối. Nó có thể mở rộng khối ngay tại chỗ, hoặc cấp phát một khối mới, sao chép dữ liệu rồi giải phóng khối cũ. Bạn không thể biết là trường hợp nào, nên con trỏ duy nhất bạn được dùng sau đó là con trỏ nó trả về.
Cách dùng con trỏ tmp
Ba dòng xoay quanh tmp chính là lý do khiến hàm này nổi tiếng. Cách viết tắt hấp dẫn kia là sai:
/* BUG: leaks the original block when realloc fails */
p = realloc(p, newsize);
if (p == NULL) {
return;
}
Khi realloc thất bại, nó trả về NULL và giữ nguyên khối ban đầu, vẫn được cấp phát và không thay đổi. Gán thẳng kết quả vào p sẽ ghi đè con trỏ duy nhất tới khối đó, nên khối ấy không bao giờ được giải phóng nữa: một vụ rò rỉ bộ nhớ, và lại xảy ra đúng lúc bộ nhớ đang khan hiếm. Hãy gán vào tmp, kiểm tra tmp, rồi mới chốt:
void *tmp = realloc(p, newsize);
if (tmp == NULL) {
/* p is still valid; handle the failure however suits the caller */
return 0;
}
p = tmp;
Thêm hai hành vi đáng biết: realloc(NULL, n) hoạt động y hệt malloc(n), nhờ đó một hàm mở rộng xử lý được lần gọi đầu tiên mà không cần trường hợp đặc biệt. Và khi mở rộng, các byte mới chưa được khởi tạo: chỉ nội dung cũ được giữ lại.
Mở rộng mảng: mẫu chuẩn
Đây là mẫu mà hầu hết các container trong C đều dùng: một độ dài, một sức chứa, và nhân đôi khi hai giá trị này gặp nhau.
Hai điểm thiết kế gánh phần chính ở đây.
Nhân đôi chứ không cộng thêm một. Mở rộng theo một lượng cố định khiến mỗi lần thêm phần tử đều phải sao chép cả mảng, nên ghi đầy n phần tử tốn khoảng n² lần sao chép byte. Nhân đôi khiến việc sao chép đủ hiếm để trung bình mỗi lần thêm chỉ tốn thời gian hằng số. Điểm khởi đầu cap == 0 ? 4 : cap * 2 xử lý lần thêm đầu tiên mà không cần nhánh riêng, vì realloc(NULL, n) chỉ là malloc.
Struct sở hữu một lần cấp phát duy nhất. v.data được giải phóng đúng một lần, bởi nơi sở hữu struct. Viết rõ quy tắc sở hữu đó ra, trong một chú thích, hoặc bằng cách luôn đi kèm mỗi vec_init với một vec_free, là điều giữ cho một container đang lớn dần không bị rò rỉ khi nó được chuyền qua các hàm.
Thu nhỏ
realloc với kích thước nhỏ hơn sẽ cắt bớt một khối, rất hữu ích sau khi bạn đã đọc một lượng dữ liệu không biết trước vào một bộ đệm quá lớn:
Thu nhỏ thất bại không phải lỗi đáng để dừng chương trình: khối cũ, lớn hơn, vẫn hợp lệ và vẫn đúng. Đây là trường hợp duy nhất mà bỏ qua thất bại là quyết định đúng, nhưng chú ý nó vẫn đi qua tmp, vì quy tắc không ghi đè con trỏ đang dùng không hề thay đổi.
Nên chọn hàm nào
| Bạn cần | Dùng |
|---|---|
| Một số byte, nội dung không quan trọng | malloc(n * sizeof *p) |
| Một số phần tử, đặt về 0 | calloc(n, sizeof *p) |
| Số lượng đến từ đầu vào không tin cậy | calloc, để có kiểm tra tràn số |
| Khối đang có, lớn hơn hoặc nhỏ hơn | realloc qua một con trỏ tmp |
| Lần cấp phát đầu tiên trong vòng lặp mở rộng | realloc(NULL, n), không cần trường hợp đặc biệt |
Cả bốn đều được giải phóng bằng cùng một free, đúng một lần.
Các lỗi thường gặp
p = realloc(p, n): rò rỉ bộ nhớ khi thất bại. Luôn đi quatmp.- Giữ một con trỏ cũ trỏ vào bên trong khối. Sau
realloc, dữ liệu có thể đã bị di chuyển, nên mọi con trỏ trỏ vào bên trong khối hay địa chỉ tính từ chỉ số đã lưu đều hết hiệu lực. Chỉ số vẫn đúng sau khi di chuyển; con trỏ thì không. - Cho rằng các byte mới bằng 0. Chỉ
callocmới đặt về 0.reallocđể vùng mở rộng ở trạng thái chưa khởi tạo. calloc(n * size, 1): cách này vứt bỏ chính phép kiểm tra tràn số mà dạng hai đối số được tạo ra để có.- Giải phóng con trỏ cũ sau khi
reallocthành công.reallocđã giải phóng nó rồi; gọifreelần nữa là giải phóng hai lần (double free).
Câu hỏi thường gặp
Khác nhau giữa calloc và malloc trong C là gì?
Có hai điểm khác. calloc(n, size) nhận số phần tử và kích thước mỗi phần tử riêng biệt, đồng thời kiểm tra để phép nhân không bị tràn số, còn malloc(n * size) có thể âm thầm quay vòng. Ngoài ra calloc ghi toàn bộ khối bằng số 0, trong khi malloc để nguyên những gì có sẵn trong vùng nhớ đó.
realloc hoạt động như thế nào trong C?
realloc(p, newsize) trả về một khối có kích thước mới, giữ nguyên nội dung hiện có trong phạm vi kích thước nhỏ hơn giữa hai khối. Nó có thể mở rộng khối tại chỗ hoặc di chuyển khối đi nơi khác, nên con trỏ được trả về là con trỏ duy nhất bạn được tiếp tục dùng: con trỏ cũ có thể đã bị giải phóng.
Vì sao nên gán kết quả realloc vào một con trỏ tạm?
Vì p = realloc(p, n) ghi đè p bằng NULL khi lời gọi thất bại, trong khi khối ban đầu vẫn được cấp phát mà không còn con trỏ nào trỏ tới: đó là rò rỉ bộ nhớ. Hãy gán vào tmp trước, kiểm tra xem nó có bằng NULL không, rồi mới viết p = tmp.
Khi nào nên dùng calloc thay vì malloc?
Khi bạn thật sự cần vùng nhớ được đặt về 0 (một mảng bộ đếm, một struct mà các trường cần bắt đầu rỗng, một bộ đệm bạn chỉ ghi một phần) hoặc khi kích thước là số phần tử nhân kích thước phần tử và có thể rất lớn. Nếu đằng nào bạn cũng ghi đè mọi byte, malloc giúp bỏ qua bước đặt về 0.