4. 动态内存分配:malloc/calloc/realloc/free 的正确使用与配对
动态内存分配,说白了就是程序运行时跟操作系统“要内存”。
你写代码时不知道数组该开多大?运行时数据量会变?那就得靠动态分配。C语言里,malloc、calloc、realloc 和 free 这四位就是干这活的。
但我得说句实话——这四位用好了是神器,用不好就是定时炸弹。我在项目中见过太多因为动态内存搞出来的崩溃,有的甚至上线几个月才暴露出来。
4.1 malloc:最基础的分配器
malloc 负责从堆上申请一块连续内存。原型很简单:
void* malloc(size_t size);
它只做一件事:给你 size 字节的空间。不初始化,里面是垃圾值。
核心要点:
- 返回
void*,需要强转成目标类型指针 - 分配失败返回
NULL——这个一定要检查 - 分配的内存内容不确定,必须手动初始化
举个例子:
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
// 处理分配失败
fprintf(stderr, "内存分配失败\n");
return -1;
}
// 使用前务必初始化
for (int i = 0; i < 10; i++) {
arr[i] = 0;
}
我个人习惯,每次 malloc 后面紧跟 NULL 检查。别嫌麻烦,你想想看,一个空指针解引用,程序直接就崩了。
4.2 calloc:带初始化的分配
calloc 和 malloc 的区别就一点:它会把分配的内存全部清零。
void* calloc(size_t num, size_t size);
参数是两个:元素个数和每个元素的大小。总大小就是 num * size。
我的建议:
如果你需要初始化为零的内存,直接用 calloc 代替 malloc + memset。一来代码更简洁,二来 calloc 内部可能做了优化,性能不一定比手动清零差。
// 分配 100 个 int,全部初始化为 0
int* data = (int*)calloc(100, sizeof(int));
if (data == NULL) {
// 处理失败
}
不过要注意,calloc 的清零是“按位清零”。对于指针类型,这意味所有指针都是 NULL,这倒是挺安全的。
4.3 realloc:动态调整大小
这是最容易被误用的一个。它的作用是调整已分配内存的大小。
void* realloc(void* ptr, size_t new_size);
逻辑是这样的:
- 如果
ptr是NULL,行为等价于malloc(new_size) - 如果
new_size是 0,行为等价于free(ptr) - 否则,尝试调整
ptr指向的内存块到new_size字节
⚠️ 我曾经踩过的坑:
千万不要这样写:
ptr = realloc(ptr, new_size); // 错误用法!
如果 realloc 失败,它会返回 NULL,但原来的内存块不会被释放。这时候 ptr 被覆盖成 NULL,原来的内存就泄漏了。
正确做法:
void* new_ptr = realloc(ptr, new_size);
if (new_ptr == NULL) {
// realloc 失败,ptr 仍然有效
free(ptr);
return -1;
}
ptr = new_ptr; // 成功后再赋值
4.4 free:释放的艺术
有借有还,再借不难。free 就是把借来的内存还回去。
void free(void* ptr);
这里有几个铁律:
| 情况 | 是否允许 | 后果 |
|---|---|---|
释放 NULL |
✅ 安全 | 无操作 |
| 释放已释放的内存 | ❌ 禁止 | 未定义行为(double free) |
| 释放栈上变量 | ❌ 禁止 | 堆损坏,程序崩溃 |
| 释放后继续使用 | ❌ 禁止 | use-after-free,安全漏洞 |
我的习惯:
每次 free 之后,立刻把指针置为 NULL:
free(ptr);
ptr = NULL;
这样即使不小心再次 free,也是安全的。虽然不能解决所有问题,但能避免大部分 double free 的尴尬。
4.5 配对原则:谁分配,谁释放
这是动态内存管理最核心的规则。说白了就是:
malloc配freecalloc配freerealloc配free
谁分配的内存,就由谁来释放。模块内部分配的内存,模块内部释放。库函数分配的内存,由库提供释放函数。
我在项目中见过最典型的问题:A 函数 malloc 了一块内存,B 函数 free 掉了,然后 A 函数还在用。这种跨模块的内存管理,最容易出问题。
避坑指南:
- 每个
malloc/calloc都要有对应的free - 使用
realloc时,用临时变量接收返回值 - 释放后立即置
NULL - 不要释放非堆内存
- 不要重复释放同一块内存
4.6 知识体系总览
下面这张图,把动态内存分配的核心逻辑串起来了:
这张图把整个流程串起来了:分配 → 检查 → 使用 → 释放 → 置空。每一步都不能省。
4.7 常见错误模式
最后,我总结几个最常见的错误,你在代码审查时可以重点盯着看:
- 内存泄漏:分配了没释放。程序跑久了内存越占越多,最后 OOM。
- 野指针:释放后没置 NULL,指针还在,但指向的内存已经还给系统了。
- 缓冲区溢出:分配了 10 个字节,写了 20 个。堆结构被破坏,崩溃是迟早的事。
- 重复释放:两个地方都调了
free,堆管理器的链表被搞乱。 - 忘记检查 NULL:分配失败直接使用,空指针解引用,段错误。
⚠️ 我曾经遇到的一个真实案例:
一个网络服务程序,上线运行了三个月,突然开始随机崩溃。查到最后,是一个回调函数里 malloc 了内存,但只在正常路径下 free,异常路径直接 return 了。每次异常泄漏 64 字节,三个月下来堆被撑爆了。
从那以后,我写代码都会在脑子里过一遍所有执行路径,确保每条路径上分配和释放都成对出现。
嗯,动态内存管理就是这样——规则不多,但每条都是血的教训换来的。记住配对原则,养成好习惯,能省去你无数调试的夜晚。
一句话总结:
谁分配,谁释放;释放后,即置空;用之前,判空否。