C 语言 calloc() 详解:零初始化、动态数组与内存管理实战

C 语言 calloc() 详解与实战 Demo

calloc() 是 C 标准库提供的动态内存分配函数,主要用于在堆上分配一块连续内存,并将分配的所有字节初始化为 0。

它与 malloc()、realloc()、free() 一起构成 C 语言动态内存管理的核心。

如果你熟悉 C#、Java,可以先这样理解:

  • malloc():申请一块内存,但不初始化其内容。
  • calloc():申请一块内存,并将所有字节清零。
  • realloc():调整已经分配的内存大小。
  • free():释放动态分配的内存。

需要注意,C 语言的动态内存分配没有 C#、Java 那样的垃圾回收机制,程序员需要自己管理内存的生命周期。

一、calloc() 基本概念

1. 函数原型

定义在 <stdlib.h> 中:

C 复制代码
#include <stdlib.h>

void *calloc(size_t nmemb, size_t size);

参数说明:

参数 类型 说明
nmemb size_t 需要分配的元素数量
size size_t 每个元素占用的字节数
返回值 void * 成功返回内存首地址,失败返回 NULL

分配的总字节数为:

Total Bytes = nmemb * size

例如:

C 复制代码
int *arr = calloc(5, sizeof(int));

假设当前平台 sizeof(int) == 4,则:

ini 复制代码
5 × 4 = 20 字节

这段代码申请了 20 字节的连续内存,并将其全部清零。

2. 内存结构示意图

注意:calloc() 清零的是字节,并不意味着所有类型的对象都获得了符合 C 标准的"零值"。例如,标准不保证所有位为 0 的指针一定表示空指针。

二、基础 Demo:使用 calloc 创建动态数组

C 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t count = 5;

    // 动态申请 5 个 int 的内存
    int *numbers = calloc(count, sizeof(*numbers));

    if (numbers == NULL) {
        perror("calloc failed");
        return EXIT_FAILURE;
    }

    // 查看初始化后的值
    for (size_t i = 0; i < count; i++) {
        printf("numbers[%zu] = %d\n", i, numbers[i]);
    }

    // 修改数组
    for (size_t i = 0; i < count; i++) {
        numbers[i] = (int)(i + 1) * 10;
    }

    printf("After modification:\n");

    for (size_t i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }

    printf("\n");

    // 释放内存
    free(numbers);
    numbers = NULL;

    return EXIT_SUCCESS;
}

运行结果:

css 复制代码
numbers[0] = 0
numbers[1] = 0
numbers[2] = 0
numbers[3] = 0
numbers[4] = 0
After modification:
10 20 30 40 50

为什么推荐 sizeof(*numbers)?

下面两种写法都可以:

C 复制代码
int *numbers = calloc(5, sizeof(int));

int *numbers = calloc(5, sizeof(*numbers));

但第二种更值得推荐。

假设以后你修改变量类型:

C 复制代码
double *numbers = calloc(5, sizeof(*numbers));

就不需要再手动修改 sizeof(int)。

另外,这里的 *numbers 出现在 sizeof 中,通常不会真正读取指针指向的内存,而是获取其指向类型的大小。

三、calloc() 与 malloc() 的区别

这是面试和实际开发中都非常常见的问题。

对比 malloc() calloc()
参数 字节数 元素数量、元素大小
初始化 不初始化 所有字节清零
返回值 void * void *
分配失败 NULL NULL
内存释放 free() free()
溢出处理 调用方自行检查乘法 检查元素数量与大小的乘积是否溢出

Demo:对比初始化行为

C 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *a = malloc(5 * sizeof(int));
    int *b = calloc(5, sizeof(int));

    if (a == NULL || b == NULL) {
        free(a);
        free(b);
        return EXIT_FAILURE;
    }

    // malloc 分配的 int 未初始化,不能直接读取
    // printf("%d\n", a[0]); // 不要这样做

    // calloc 分配的 int 初始化为 0
    for (int i = 0; i < 5; i++) {
        printf("%d ", b[i]);
    }

    free(a);
    free(b);

    return 0;
}

这里特别强调:

malloc() 分配的内存不是一定包含随机数,也不是一定包含 0,而是内容未初始化,不能依赖其值。

calloc() 在需要零初始化时通常更方便。

calloc() 一定比 malloc() 慢吗?

不一定。

虽然 calloc() 需要保证内存清零,但现代操作系统和内存分配器可能使用按需映射的零页等优化。

因此:

  • 小块内存分配,两者性能差距可能很小。
  • 大块内存分配,实际性能取决于分配器和访问模式。
  • malloc() + memset() 不一定比 calloc() 快。
  • 需要零初始化时,优先考虑 calloc(),不必为了猜测性能而手动清零。

四、实战 Demo 1:动态结构体数组

假设你开发一个学生管理系统,需要根据用户输入动态创建学生数组。

C 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int id;
    char name[50];
    double score;
} Student;

int main(void) {
    size_t count = 3;

    Student *students = calloc(count, sizeof(*students));

    if (students == NULL) {
        perror("calloc");
        return EXIT_FAILURE;
    }

    students[0] = (Student){
        .id = 1,
        .name = "Alice",
        .score = 95.5
    };

    students[1] = (Student){
        .id = 2,
        .name = "Bob",
        .score = 88.0
    };

    students[2] = (Student){
        .id = 3,
        .name = "Charlie",
        .score = 92.5
    };

    for (size_t i = 0; i < count; i++) {
        printf(
            "ID: %d, Name: %s, Score: %.1f\n",
            students[i].id,
            students[i].name,
            students[i].score
        );
    }

    free(students);
    students = NULL;

    return EXIT_SUCCESS;
}

输出:

yaml 复制代码
ID: 1, Name: Alice, Score: 95.5
ID: 2, Name: Bob, Score: 88.0
ID: 3, Name: Charlie, Score: 92.5

这里的 students 指向一块连续的结构体数组内存。

这与 C# 中的 Student[] 在使用体验上有一定相似性,但 C 语言需要手动释放内存。

五、实战 Demo 2:二维动态数组

在实际项目中,经常需要动态创建二维数组,例如:

  • 图像像素矩阵
  • 游戏地图
  • 数学矩阵
  • 表格数据

推荐的一种方式是申请一块连续内存。

C 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t rows = 3;
    size_t cols = 4;

    // 动态二维数组,指向含 4 个 int 的数组
    int (*matrix)[4] = calloc(rows, sizeof(*matrix));

    if (matrix == NULL) {
        perror("calloc");
        return EXIT_FAILURE;
    }

    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < cols; j++) {
            matrix[i][j] = (int)(i * cols + j + 1);
        }
    }

    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < cols; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n");
    }

    free(matrix);

    return EXIT_SUCCESS;
}

输出:

复制代码
  1   2   3   4
  5   6   7   8
  9  10  11  12

int (*matrix)[4] 是什么?

这是一个指向包含 4 个 int 元素的数组的指针。

与下面的类型不同:

perl 复制代码
int *p;          // 指向一个 int
int *p2[4];      // 4 个 int* 组成的数组
int (*p3)[4];    // 指向 int[4] 数组的指针

其中 matrix[i][j] 能正确访问二维数组,是因为编译器知道每行包含 4 个 int。

如果行列数都需要运行时确定,在支持可变长度数组的 C99 编译器中,可以使用:

C 复制代码
size_t rows = 3;
size_t cols = 4;

int (*matrix)[cols] = calloc(rows, sizeof(*matrix));

不过,可变长度数组是可选特性,并非所有编译器都支持。

六、实战 Demo 3:动态扩容数组(calloc + realloc)

这是实际开发中更有价值的例子。

实现一个类似 C# List<int> 的动态数组。

C 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} IntList;

// 初始化
int list_init(IntList *list, size_t capacity) {
    if (capacity == 0) {
        capacity = 4;
    }

    list->data = calloc(capacity, sizeof(*list->data));

    if (list->data == NULL) {
        return -1;
    }

    list->size = 0;
    list->capacity = capacity;

    return 0;
}

// 添加元素
int list_add(IntList *list, int value) {
    if (list->size == list->capacity) {
        if (list->capacity > SIZE_MAX / 2) {
            return -1;
        }

        size_t new_capacity = list->capacity * 2;

        if (new_capacity > SIZE_MAX / sizeof(*list->data)) {
            return -1;
        }

        int *new_data = realloc(
            list->data,
            new_capacity * sizeof(*list->data)
        );

        if (new_data == NULL) {
            return -1;
        }

        list->data = new_data;
        list->capacity = new_capacity;
    }

    list->data[list->size++] = value;
    return 0;
}

// 释放
void list_free(IntList *list) {
    free(list->data);

    list->data = NULL;
    list->size = 0;
    list->capacity = 0;
}

int main(void) {
    IntList list = {0};

    if (list_init(&list, 2) != 0) {
        return EXIT_FAILURE;
    }

    for (int i = 1; i <= 10; i++) {
        if (list_add(&list, i * 10) != 0) {
            list_free(&list);
            return EXIT_FAILURE;
        }
    }

    printf("Size: %zu\n", list.size);
    printf("Capacity: %zu\n", list.capacity);

    for (size_t i = 0; i < list.size; i++) {
        printf("%d ", list.data[i]);
    }

    printf("\n");

    list_free(&list);
    return EXIT_SUCCESS;
}

输出:

makefile 复制代码
Size: 10
Capacity: 16
10 20 30 40 50 60 70 80 90 100

扩容过程

这里需要注意:

realloc() 扩容后,新增的内存不会自动清零。

例如从容量 4 扩容到 8,新增的 4 个元素内容未初始化。

但这个 Demo 不受影响,因为每次添加元素时都会先写入对应位置。

另外,realloc() 可能返回不同的内存地址,因此必须使用它返回的新指针,而不能继续使用旧指针。

七、实战 Demo 4:实现一个简单的内存池

假设你需要管理大量固定大小的对象,例如:

  • 网络连接上下文
  • 游戏实体
  • 任务节点
  • 消息队列节点

可以使用 calloc() 一次性申请多个对象,减少反复调用内存分配器的次数。

C 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int id;
    int active;
    char buffer[256];
} Connection;

typedef struct {
    Connection *items;
    size_t capacity;
} ConnectionPool;

int pool_init(ConnectionPool *pool, size_t capacity) {
    pool->items = calloc(capacity, sizeof(Connection));

    if (pool->items == NULL) {
        return -1;
    }

    pool->capacity = capacity;
    return 0;
}

Connection *pool_acquire(ConnectionPool *pool) {
    for (size_t i = 0; i < pool->capacity; i++) {
        if (!pool->items[i].active) {
            pool->items[i].active = 1;
            pool->items[i].id = (int)i + 1;

            return &pool->items[i];
        }
    }

    return NULL;
}

void pool_release(Connection *conn) {
    conn->active = 0;
}

void pool_destroy(ConnectionPool *pool) {
    free(pool->items);
    pool->items = NULL;
    pool->capacity = 0;
}

int main(void) {
    ConnectionPool pool = {0};

    if (pool_init(&pool, 3) != 0) {
        return EXIT_FAILURE;
    }

    Connection *c1 = pool_acquire(&pool);
    Connection *c2 = pool_acquire(&pool);

    if (c1 != NULL && c2 != NULL) {
        printf("Connection %d acquired\n", c1->id);
        printf("Connection %d acquired\n", c2->id);

        pool_release(c1);
        printf("Connection %d released\n", c1->id);
    }

    pool_destroy(&pool);

    return EXIT_SUCCESS;
}

输出:

复制代码
Connection 1 acquired
Connection 2 acquired
Connection 1 released

这个 Demo 是一个简化的固定容量对象池。

实际生产环境中还需要考虑线程安全、对象重置、资源清理以及避免释放后继续使用对象等问题。

特别是 pool_release() 只是把对象标记为空闲,并没有清空 buffer。如果对象可能存放敏感数据,重新分配时还需要做好数据清理。

八、calloc() 的常见错误

1. 忘记 free(),造成内存泄漏

错误:

C 复制代码
void test(void) {
    int *p = calloc(100, sizeof(int));

    // 使用 p ...

    // 忘记 free(p)
}

每次调用都会申请内存,但没有释放。

正确:

C 复制代码
void test(void) {
    int *p = calloc(100, sizeof(int));

    if (p == NULL) {
        return;
    }

    // 使用 p ...

    free(p);
}

2. 释放后继续使用

错误:

C 复制代码
int *p = calloc(5, sizeof(int));

free(p);

p[0] = 100;  // Use After Free,未定义行为

正确:

C 复制代码
free(p);
p = NULL;

注意:设置 p = NULL 只能防止通过这个变量继续访问,不能让其他指向同一块内存的指针自动失效。

3. 重复释放

错误:

C 复制代码
free(p);
free(p);  // Double Free,未定义行为

正确:

C 复制代码
free(p);
p = NULL;

free(p);  // free(NULL) 是安全的

4. 数组越界

错误:

C 复制代码
int *p = calloc(5, sizeof(int));

p[5] = 100;  // 越界

合法索引只有 0 到 4。

5. 分配 0 字节

C 复制代码
int *p = calloc(0, sizeof(int));

这是需要特别注意的情况。

C 标准允许零大小分配具有实现定义的行为,返回值可能是 NULL,也可能是一个不能用于访问元素的非空指针。

因此,实际开发中建议显式处理:

C 复制代码
if (count == 0) {
    return;
}

6. 整数溢出

下面的写法存在风险:

C 复制代码
size_t count = /* 外部输入 */;

int *p = malloc(count * sizeof(int));

如果乘法发生无符号整数溢出,可能申请到比预期更小的内存,导致后续越界写入。

相比之下:

C 复制代码
int *p = calloc(count, sizeof(int));

calloc() 会检查元素数量与元素大小相乘是否超出可表示的范围,并在无法满足分配时返回 NULL。

当然,分配成功也不代表可以忽略数组下标和实际容量检查。

九、malloc、calloc、realloc、free 怎么选?

场景 推荐函数
申请一块内存,准备完整写入 malloc()
创建初始值为 0 的整数数组 calloc()
创建需要字节清零的缓冲区 calloc()
扩大或缩小已有动态数组 realloc()
释放动态内存 free()
创建并初始化复杂对象 分配内存后显式初始化

最后补充一个与 C#、Java 很不一样的知识点:calloc() 只负责分配和清零内存,不负责执行任何构造函数,也不负责自动释放对象。

相关推荐
David猪大卫1 小时前
【C++修炼】STL容器—stack与queue
c语言·开发语言·数据结构·c++·经验分享·笔记·学习
瑶池酒剑仙1 小时前
C++变量初始化五法完全指南:默认/拷贝/直接/列表/值初始化与窄化转换陷阱(青云之志)
c语言·开发语言·数据结构·c++·算法·visual studio
沐晓时光3 小时前
C语言入门,深入理解指针(6)
c语言·开发语言
Navigator_Z3 小时前
LeetCode //C - 1306. Jump Game III
c语言·算法·leetcode
Zwawa17 小时前
编译器工具链:GCC、Clang、MSVC、ICC、armclang
c语言·嵌入式·编译器
学逆向的17 小时前
C 指针常量与常量指针
c语言·开发语言·网络安全
水饺编程18 小时前
第2章:Linux 基础,exit 命令
linux·c语言·汇编
极客 - L U20 小时前
卡尔曼滤波从零到精通
c语言·数据结构·单片机·嵌入式硬件·mcu
谙弆悕博士1 天前
用 C 写了个 Windows 聊天室,踩了几个多线程和控制台的坑
c语言·开发语言·c++·windows·网络编程·聊天室