动态数组:malloc、calloc、realloc 与柔性数组
说实话,C语言的数组有个让人头疼的地方——你得在编译时就定好大小。我早年做嵌入式项目时,经常遇到这种情况:预估了100个缓冲区,结果数据量一上来就崩了。后来我才真正搞懂动态内存分配,这玩意儿说白了就是让程序在运行时按需申请内存,想多大就多大。
今天咱们就聊聊动态数组的三大法宝:malloc、calloc、realloc,以及结构体里那个很巧妙的柔性数组。嗯,这些都是我实际项目中反复用到的,踩过的坑也不少。
为什么需要动态数组?
静态数组的大小在编译时就固定了。你想想看,如果用户输入的数据量不确定,你怎么办?
- 开大了——浪费内存
- 开小了——程序崩溃
动态数组就解决了这个问题。程序跑起来后,根据实际需要去堆上申请内存。用多少,申请多少。
核心区别:静态数组在栈上分配,动态数组在堆上分配。栈空间有限(通常几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:分配并清零
calloc 和 malloc 很像,但它多做了两件事:
- 自动将分配的内存全部初始化为 0
- 参数是「元素个数」和「每个元素大小」,更直观
int *arr = (int*)calloc(10, sizeof(int));
// 等价于 malloc + memset(arr, 0, 10 * sizeof(int))
我个人在分配结构体数组时,特别喜欢用 calloc。为什么呢?因为结构体里的指针成员如果没初始化,默认是 NULL,而不是野指针。这能省去很多麻烦。
小技巧:如果你需要清零的内存,用 calloc 比 malloc + 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 |
不需要清零,省去一次 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;
}
这个例子把 malloc、realloc、free 都用上了。你想想看,如果一开始就固定一个 1024 字节的数组,大部分时候用不完,偶尔又不够用。动态扩展就灵活多了。
嗯,动态数组这块内容就聊到这儿。记住一点:用动态内存分配,一定要记得 free。我见过太多内存泄漏的 bug,都是因为只分配不释放。写代码时养成好习惯:分配的同时就写好释放的逻辑。
公众号:蓝海资料掘金营,微信deep3321