calloc
Sau bài này bạn sẽ làm được
- Phân biệt calloc với malloc trên ba tiêu chí
- Biết vì sao calloc an toàn hơn với phép nhân kích thước
- Hiểu vì sao bit 0 không phải lúc nào cũng là giá trị 0
- Chọn đúng hàm cho từng tình huống
Hàm calloc làm hai việc mà bạn hay phải làm liền nhau: cấp phát và xóa về 0. Nó cũng kiểm tra tràn số hộ bạn. Nhưng nó có một giới hạn quan trọng mà rất ít tài liệu nói tới, và bài này sẽ nói.
#Cách dùng
#include <stdlib.h>
void *calloc(size_t n, size_t co);Cấp phát chỗ cho n phần tử, mỗi phần tử co byte, rồi điền 0 vào toàn bộ. Trả về NULL khi thất bại.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t n = 10;
int *a = calloc(n, sizeof *a);
if (a == NULL) {
fprintf(stderr, "Khong du bo nho\n");
return 1;
}
for (size_t i = 0; i < n; ++i)
printf("%d ", a[i]); /* chắc chắn in ra mười số 0 */
printf("\n");
free(a); /* giải phóng bằng free, giống malloc */
return 0;
}0 0 0 0 0 0 0 0 0 0
#Ba khác biệt với malloc
| malloc | calloc | |
|---|---|---|
| Số tham số | Một, tổng số byte | Hai, số phần tử và kích thước mỗi phần tử |
| Nội dung sau khi cấp | Rác | Toàn bit 0 |
| Kiểm tra tràn phép nhân | Bạn tự lo | Hàm tự kiểm tra và trả về NULL |
| Tốc độ với khối nhỏ | Nhanh hơn chút | Chậm hơn vì phải xóa |
| Tốc độ với khối rất lớn | Nhanh | Cũng nhanh, xem mục hiệu năng |
| Giải phóng bằng | free | free, giống hệt |
Khác biệt 1: kiểm tra tràn
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t n = 4611686018427387905u; /* 0x4000000000000001 */
int *a = malloc(n * sizeof *a); /* phép nhân tràn thành 4 */
int *b = calloc(n, sizeof *a); /* calloc phát hiện và trả về NULL */
printf("malloc: %s\n", a ? "co con tro, NHUNG CHI 4 BYTE" : "NULL");
printf("calloc: %s\n", b ? "co con tro" : "NULL, dung");
free(a);
free(b);
return 0;
}malloc: co con tro, NHUNG CHI 4 BYTE calloc: NULL, dung
Khác biệt 2: nội dung bằng 0
typedef struct {
char ten[64];
int tuoi;
double diem;
} SinhVien;
SinhVien *sv = malloc(sizeof *sv);
if (sv == NULL) return 1;
sv->tuoi = 20;
printf("%s\n", sv->ten); /* ten chứa rác, không phải chuỗi hợp lệ *//* Cách 1: calloc */
SinhVien *sv = calloc(1, sizeof *sv);
/* Cách 2: malloc rồi memset */
SinhVien *sv2 = malloc(sizeof *sv2);
if (sv2 != NULL)
memset(sv2, 0, sizeof *sv2);
/* Cách 3: gán từ một struct đã khởi tạo, rõ ràng nhất */
SinhVien *sv3 = malloc(sizeof *sv3);
if (sv3 != NULL)
*sv3 = (SinhVien){ 0 };Khác biệt 3: tốc độ
malloc : 0.031 s calloc : 0.048 s malloc + memset: 0.052 s
malloc : 0.000012 s calloc : 0.000015 s malloc + memset: 0.213 s
Kết quả thứ hai gây bất ngờ: với khối rất lớn, calloc nhanh hơn hàng chục nghìn lần so với malloc cộng memset. Mục sau giải thích tại sao.
#Bit 0 không phải lúc nào cũng là giá trị 0
Chuẩn C nói rằng calloc điền bit 0 vào toàn bộ vùng nhớ. Với phần lớn kiểu thì bit 0 đúng là giá trị 0, nhưng chuẩn không bảo đảm điều đó cho mọi kiểu.
| Kiểu | Bit 0 có phải giá trị 0 không | Ghi chú |
|---|---|---|
| int, long, char và mọi kiểu nguyên | Có | Chuẩn bảo đảm |
| float, double | Trong thực tế có | Chuẩn không bảo đảm, nhưng mọi máy dùng IEEE 754 đều đúng |
| Con trỏ | Trong thực tế có | Chuẩn không bảo đảm NULL được biểu diễn bằng toàn bit 0 |
| bool trong stdbool.h | Có | false đúng là toàn bit 0 |
| Trường bit và struct có đệm | Phần đệm không xác định | Nhưng phần đệm không ảnh hưởng tới giá trị các trường |
#Hiệu năng và thủ thuật trang nhớ
Vì sao calloc một gigabyte lại nhanh hơn malloc cộng memset hàng chục nghìn lần? Câu trả lời nằm ở hệ điều hành.
| Bước | malloc + memset | calloc với khối lớn |
|---|---|---|
| 1 | Xin hệ điều hành một gigabyte | Xin hệ điều hành một gigabyte |
| 2 | Hệ điều hành hứa cấp, chưa đụng vào bộ nhớ thật | Hệ điều hành hứa cấp, và các trang này đã được bảo đảm bằng 0 |
| 3 | memset chạm vào từng byte, buộc hệ điều hành cấp thật từng trang | calloc biết trang mới luôn bằng 0 nên KHÔNG chạm vào gì cả |
| 4 | Một gigabyte bộ nhớ vật lý bị dùng ngay | Bộ nhớ vật lý chỉ bị dùng khi bạn thật sự ghi vào |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv)
{
size_t co = 1024UL * 1024 * 1024; /* 1 GB */
char *p = calloc(co, 1);
if (p == NULL) return 1;
printf("Da cap 1 GB. Xem bo nho tien trinh luc nay.\n");
getchar();
if (argc > 1) {
memset(p, 1, co); /* chạm vào mọi trang */
printf("Da cham vao tat ca. Xem lai bo nho tien trinh.\n");
getchar();
}
free(p);
return 0;
}RSS VSZ CMD 1664 1051564 ./lazy # ảo 1 GB nhưng thật chỉ 1.6 MB
RSS VSZ CMD 1050112 1051564 ./lazy # sau memset, thật sự chiếm 1 GB
#Chọn hàm nào
| Tình huống | Nên dùng | Vì sao |
|---|---|---|
| Mảng mà bạn sẽ ghi đè toàn bộ ngay | malloc | Không cần xóa, và bạn tự kiểm tra tràn |
| Mảng số bắt đầu từ 0, ví dụ bộ đếm | calloc | Được xóa sẵn, đúng thứ bạn cần |
| Struct có con trỏ bên trong | calloc | Con trỏ bằng NULL nên kiểm tra được, thay vì chứa rác |
| Số phần tử đến từ dữ liệu bên ngoài | calloc | Tự kiểm tra tràn phép nhân |
| Bộ đệm rất lớn dùng thưa thớt | calloc | Tận dụng cấp phát lười, chỉ tốn bộ nhớ vật lý cho phần thật sự dùng |
| Một đối tượng đơn lẻ, gán ngay mọi trường | malloc | Xóa rồi gán lại là làm hai lần |
Bảng tổng hợp bốn hàm cấp phát
| Hàm | Làm gì | Trả về NULL khi |
|---|---|---|
| malloc(co) | Cấp co byte, nội dung là rác | Không đủ bộ nhớ |
| calloc(n, co) | Cấp n nhân co byte, xóa về bit 0 | Không đủ bộ nhớ, hoặc phép nhân tràn |
| realloc(p, co) | Đổi kích thước vùng p thành co byte | Không đủ bộ nhớ, và khi đó p vẫn nguyên |
| free(p) | Trả vùng p về bộ cấp phát | Không trả về gì |
/* Bốn hàm này đều khai báo trong stdlib.h và đều đi cùng free.
Không bao giờ trộn với hàm cấp phát của hệ điều hành như mmap. */
int *a = malloc(n * sizeof *a);
int *b = calloc(n, sizeof *b);
int *c = realloc(a, 2 * n * sizeof *c);
free(b);
free(c); /* a đã bị realloc tiếp quản, KHÔNG free a nữa */Dòng cuối là chỗ dễ sai nhất, và Bài 14.8 sẽ nói kỹ về nó.
Tự làm thử
- Cấp phát mảng bằng
mallocvà bằngcalloc, in nội dung ban đầu của cả hai. - Gọi
mallocvàcallocvới số phần tử gây tràn phép nhân, xác nhận chỉcalloctrả vềNULL. - Cấp phát struct có con trỏ bằng cả hai hàm, in giá trị con trỏ bên trong và giải thích chênh lệch.
- Đo thời gian cấp phát một triệu khối nhỏ bằng ba cách trong bài, lập bảng kết quả.
- Cấp phát một gigabyte bằng
calloc, theo dõi cột RSS trước và sau khi gọimemset. - Viết hàm bọc
cap_va_xoatương đươngcallocnhưng dùngmalloc, đo chênh lệch tốc độ với khối một gigabyte.
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
callocnhận hai tham số, xóa vùng nhớ về bit 0, và tự kiểm tra tràn khi nhân hai tham số đó.- Việc tự kiểm tra tràn là lý do mạnh nhất để dùng nó khi số phần tử đến từ dữ liệu bên ngoài.
- Chuẩn chỉ bảo đảm bit 0, không bảo đảm con trỏ rỗng và số thực 0.0 là toàn bit 0. Trong thực tế thì đúng.
- Với khối rất lớn,
callocnhanh hơnmalloccộngmemsethàng chục nghìn lần nhờ cơ chế cấp phát trang lười biếng. - Dùng
calloclàm mặc định, chỉ chuyển sangmallockhi đã đo được lợi ích. Cả hai đều giải phóng bằngfree.