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

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.

calloc.c
#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;
}
terminal
./calloc
0 0 0 0 0 0 0 0 0 0

#Ba khác biệt với malloc

malloccalloc
Số tham sốMột, tổng số byteHai, số phần tử và kích thước mỗi phần tử
Nội dung sau khi cấpRácToàn bit 0
Kiểm tra tràn phép nhânBạn tự loHàm tự kiểm tra và trả về NULL
Tốc độ với khối nhỏNhanh hơn chútChậm hơn vì phải xóa
Tốc độ với khối rất lớnNhanhCũng nhanh, xem mục hiệu năng
Giải phóng bằngfreefree, giống hệt

Khác biệt 1: kiểm tra tràn

tran.c
#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;
}
terminal
./tran
malloc: co con tro, NHUNG CHI 4 BYTE
calloc: NULL, dung

Khác biệt 2: nội dung bằng 0

malloc rồi quên xóa
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ệ */
Hai cách đúng
/* 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 độ

terminal
# Cấp phát một triệu khối 64 byte
./do-toc-do
malloc         : 0.031 s
calloc         : 0.048 s
malloc + memset: 0.052 s
# Cấp phát một khối 1 gigabyte
./do-lon
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ểuBit 0 có phải giá trị 0 khôngGhi chú
int, long, char và mọi kiểu nguyênChuẩn bảo đảm
float, doubleTrong 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.hfalse đúng là toàn bit 0
Trường bit và struct có đệmPhần đệm không xác địnhNhư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ướcmalloc + memsetcalloc với khối lớn
1Xin hệ điều hành một gigabyteXin hệ điều hành một gigabyte
2Hệ điều hành hứa cấp, chưa đụng vào bộ nhớ thậtHệ điều hành hứa cấp, và các trang này đã được bảo đảm bằng 0
3memset chạm vào từng byte, buộc hệ điều hành cấp thật từng trangcalloc biết trang mới luôn bằng 0 nên KHÔNG chạm vào gì cả
4Một gigabyte bộ nhớ vật lý bị dùng ngayBộ nhớ vật lý chỉ bị dùng khi bạn thật sự ghi vào
lazy.c
#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;
}
terminal
# Cột RSS là bộ nhớ vật lý thật sự đang dùng
./lazy & ps -o rss,vsz,cmd -p $!
  RSS    VSZ CMD
 1664 1051564 ./lazy      # ảo 1 GB nhưng thật chỉ 1.6 MB
./lazy cham & ps -o rss,vsz,cmd -p $!
  RSS     VSZ CMD
1050112 1051564 ./lazy    # sau memset, thật sự chiếm 1 GB

#Chọn hàm nào

Tình huốngNên dùngVì sao
Mảng mà bạn sẽ ghi đè toàn bộ ngaymallocKhô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ộ đếmcallocĐược xóa sẵn, đúng thứ bạn cần
Struct có con trỏ bên trongcallocCon 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àicallocTự kiểm tra tràn phép nhân
Bộ đệm rất lớn dùng thưa thớtcallocTậ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ườngmallocXó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àmLàm gìTrả về NULL khi
malloc(co)Cấp co byte, nội dung là rácKhông đủ bộ nhớ
calloc(n, co)Cấp n nhân co byte, xóa về bit 0Khô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 byteKhông đủ bộ nhớ, và khi đó p vẫn nguyên
free(p)Trả vùng p về bộ cấp phátKhô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ử

  1. Cấp phát mảng bằng malloc và bằng calloc, in nội dung ban đầu của cả hai.
  2. Gọi malloccalloc với số phần tử gây tràn phép nhân, xác nhận chỉ calloc trả về NULL.
  3. 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.
  4. Đ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ả.
  5. Cấp phát một gigabyte bằng calloc, theo dõi cột RSS trước và sau khi gọi memset.
  6. Viết hàm bọc cap_va_xoa tương đương calloc nhưng dùng malloc, đ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

  • calloc nhậ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, calloc nhanh hơn malloc cộng memset hàng chục nghìn lần nhờ cơ chế cấp phát trang lười biếng.
  • Dùng calloc làm mặc định, chỉ chuyển sang malloc khi đã đo được lợi ích. Cả hai đều giải phóng bằng free.