动态数组:malloc、calloc、realloc 与柔性数组

说实话,C语言的数组有个让人头疼的地方——你得在编译时就定好大小。我早年做嵌入式项目时,经常遇到这种情况:预估了100个缓冲区,结果数据量一上来就崩了。后来我才真正搞懂动态内存分配,这玩意儿说白了就是让程序在运行时按需申请内存,想多大就多大。

今天咱们就聊聊动态数组的三大法宝:malloccallocrealloc,以及结构体里那个很巧妙的柔性数组。嗯,这些都是我实际项目中反复用到的,踩过的坑也不少。

为什么需要动态数组?

静态数组的大小在编译时就固定了。你想想看,如果用户输入的数据量不确定,你怎么办?

  • 开大了——浪费内存
  • 开小了——程序崩溃

动态数组就解决了这个问题。程序跑起来后,根据实际需要去堆上申请内存。用多少,申请多少。

核心区别:静态数组在栈上分配,动态数组在堆上分配。栈空间有限(通常几MB),堆空间大得多(接近物理内存上限)。

malloc:最基础的内存分配

malloc 的全称是 memory allocation。它的用法很简单:传入你要的字节数,返回一个指向这块内存的指针。

int *arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
    // 处理内存分配失败
    fprintf(stderr, "内存分配失败\n");
    exit(1);
}

这里有个细节:malloc 返回的是 void*,所以需要强制类型转换。我个人习惯在 C 代码里显式转换,虽然 C 语言允许隐式转换,但显式写出来更清晰。

注意:malloc 分配的内存是未初始化的。里面的数据是随机值,千万别直接拿来用!我曾经见过一个同事,用 malloc 分配数组后直接遍历,结果全是垃圾值,排查了半天才发现问题。

calloc:分配并清零

callocmalloc 很像,但它多做了两件事:

  1. 自动将分配的内存全部初始化为 0
  2. 参数是「元素个数」和「每个元素大小」,更直观
int *arr = (int*)calloc(10, sizeof(int));
// 等价于 malloc + memset(arr, 0, 10 * sizeof(int))

我个人在分配结构体数组时,特别喜欢用 calloc。为什么呢?因为结构体里的指针成员如果没初始化,默认是 NULL,而不是野指针。这能省去很多麻烦。

小技巧:如果你需要清零的内存,用 callocmalloc + memset 更快。因为操作系统在分配物理页时,如果要求清零,可以直接使用预清零的页面。

realloc:动态调整大小

这才是动态数组的精髓。程序跑着跑着,发现数组不够用了,怎么办?realloc 就是干这个的。

int *arr = (int*)malloc(5 * sizeof(int));
// ... 用了5个元素,还需要更多
int *temp = (int*)realloc(arr, 10 * sizeof(int));
if (temp == NULL) {
    // realloc 失败,原内存仍然有效
    free(arr);
    exit(1);
}
arr = temp;

这里有个坑,我踩过不止一次:realloc 可能会移动内存块。如果它在新位置分配了内存,原来的指针就失效了。所以一定要用临时变量接收返回值,别直接 arr = realloc(arr, ...),万一失败了你连原数据都丢了。

realloc 的行为:

  • 如果当前内存块后面有足够的空间,直接扩展,返回原指针
  • 如果后面空间不够,重新找一块更大的内存,拷贝旧数据,释放旧内存
  • 如果新大小比旧大小还小,可能会缩小(但不保证释放多余内存)

柔性数组:结构体里的动态尾巴

柔性数组是 C99 引入的特性。它允许结构体的最后一个成员是一个未指定大小的数组。说白了,就是结构体后面可以挂一个动态长度的数组。

struct flex_array {
    int length;
    int data[];  // 柔性数组成员,不占结构体大小
};

// 使用方式
int n = 20;
struct flex_array *fa = (struct flex_array*)malloc(
    sizeof(struct flex_array) + n * sizeof(int)
);
fa->length = n;
for (int i = 0; i < n; i++) {
    fa->data[i] = i * 2;
}

注意看,data[] 不占用结构体的 sizeof。你申请内存时,要手动加上数组需要的空间。这样做的好处是:所有数据都在一块连续内存里,访问效率高,而且只需要一次 free 就能释放全部。

限制条件:

  • 柔性数组必须是结构体的最后一个成员
  • 一个结构体只能有一个柔性数组
  • 不能作为结构体数组的元素(因为每个元素大小不确定)

知识体系图

下面这张图帮你理清动态数组的核心脉络:

动态数组核心知识体系 动态内存分配 分配 调整 结构体应用 malloc 分配未初始化内存 calloc 分配并清零 realloc 调整已有内存大小 柔性数组 结构体尾部动态数组 ⚠️ 避坑指南 • malloc 返回的指针必须检查是否为 NULL • realloc 要用临时变量接收返回值,防止内存泄漏 • 柔性数组必须是结构体最后一个成员,且不能用于数组

实际项目中的选择

说了这么多,到底什么时候用哪个?我根据自己的经验整理了一下:

场景 推荐函数 原因
分配新数组,后续会逐个赋值 malloc 不需要清零,省去一次 memset 的开销
分配结构体数组,需要安全默认值 calloc 指针成员自动初始化为 NULL,避免野指针
数组大小动态增长(如读取文件) realloc 按需扩展,避免一次性申请过多内存
结构体需要携带变长数据 柔性数组 内存连续,一次分配一次释放,效率高

我的习惯:在不确定大小时,先分配一个较小的初始容量(比如 16 个元素),然后每次不够时翻倍扩展。这样平均时间复杂度是 O(1),比每次只加一个元素高效得多。

一个完整的例子

最后,给你看一个我实际项目中用过的模式——动态字符串拼接:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *data;
    size_t len;
    size_t cap;
} StringBuilder;

StringBuilder* sb_create(size_t init_cap) {
    StringBuilder *sb = (StringBuilder*)malloc(sizeof(StringBuilder));
    sb->data = (char*)malloc(init_cap);
    sb->data[0] = '\0';
    sb->len = 0;
    sb->cap = init_cap;
    return sb;
}

void sb_append(StringBuilder *sb, const char *str) {
    size_t add_len = strlen(str);
    if (sb->len + add_len + 1 > sb->cap) {
        // 容量不够,翻倍扩展
        while (sb->len + add_len + 1 > sb->cap) {
            sb->cap *= 2;
        }
        char *new_data = (char*)realloc(sb->data, sb->cap);
        if (new_data == NULL) {
            fprintf(stderr, "扩展失败\n");
            exit(1);
        }
        sb->data = new_data;
    }
    memcpy(sb->data + sb->len, str, add_len + 1);
    sb->len += add_len;
}

void sb_free(StringBuilder *sb) {
    free(sb->data);
    free(sb);
}

int main() {
    StringBuilder *sb = sb_create(16);
    sb_append(sb, "Hello, ");
    sb_append(sb, "动态数组!");
    printf("%s\n", sb->data);  // 输出:Hello, 动态数组!
    sb_free(sb);
    return 0;
}

这个例子把 mallocreallocfree 都用上了。你想想看,如果一开始就固定一个 1024 字节的数组,大部分时候用不完,偶尔又不够用。动态扩展就灵活多了。

嗯,动态数组这块内容就聊到这儿。记住一点:用动态内存分配,一定要记得 free。我见过太多内存泄漏的 bug,都是因为只分配不释放。写代码时养成好习惯:分配的同时就写好释放的逻辑。


公众号:蓝海资料掘金营,微信deep3321