10、动态内存管理:malloc/calloc/realloc 的使用错误、内存泄漏、双重释放、释放后使用(UAF)
动态内存管理,说白了就是 C 语言里最让人又爱又恨的东西。爱它,是因为没有它,很多程序根本写不出来;恨它,是因为一旦用错,bug 能让你调试到怀疑人生。我做了十几年嵌入式开发,敢说至少一半的线上崩溃,根因都在堆内存上。
今天咱们就把这块硬骨头啃下来。我会把常见的陷阱一个个拆开,配上我踩过的坑,希望能帮你少走弯路。
10.1 malloc、calloc、realloc 的正确用法
这三个函数是动态内存的基石。但很多人用了一辈子,也没用对。
10.1.1 malloc:最基础,也最容易出错
malloc 分配一块连续内存,返回 void*。原型很简单:
void *malloc(size_t size);
但这里有个坑——它不会初始化内存。分配出来的内存里是随机值,可能是 0,也可能是 0xDEADBEEF。我见过有人直接拿 malloc 出来的内存当零初始化用,结果程序跑起来时好时坏,查了两天才发现是这里的问题。
正确的做法是:
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理分配失败
return -1;
}
// 手动初始化
memset(p, 0, 10 * sizeof(int));
10.1.2 calloc:自动清零,但小心性能
calloc 和 malloc 的区别在于:它会自动把内存清零。原型:
void *calloc(size_t nmemb, size_t size);
我个人习惯在分配数组或结构体时优先用 calloc。为什么?因为清零能避免很多“野指针”和“未初始化”的问题。尤其是嵌入式环境里,内存残留数据可能来自上一次任务,不清零的话后果很严重。
但要注意:calloc 比 malloc 慢,因为它多了一步清零操作。如果你分配的内存马上会被覆盖,用 malloc 更合适。
10.1.3 realloc:扩容有风险,使用需谨慎
realloc 用来调整已分配内存的大小。原型:
void *realloc(void *ptr, size_t new_size);
这里有个经典陷阱:realloc 失败会返回 NULL,但原来的内存不会被释放。很多人这样写:
p = (int *)realloc(p, new_size * sizeof(int));
如果 realloc 失败,p 变成 NULL,原来的内存就泄漏了!正确的写法是:
int *new_p = (int *)realloc(p, new_size * sizeof(int));
if (new_p == NULL) {
// 处理失败,但 p 仍然有效
free(p);
return -1;
}
p = new_p;
10.2 内存泄漏:看不见的杀手
内存泄漏,说白了就是“借了不还”。你 malloc 了一块内存,用完了没 free,这块内存就再也找不回来了。程序跑得越久,泄漏越多,最后系统崩溃。
我在项目中遇到过最离谱的一次:一个通信模块,每次收到消息都 malloc 一个缓冲区,但处理完忘了 free。测试时跑几分钟没问题,上线后跑了三天,系统内存耗尽,直接死机。查日志才发现,内存占用曲线一直在缓慢上升。
避免内存泄漏的几个原则:
- 谁分配,谁释放:模块内部 malloc 的内存,由模块内部 free。
- 成对出现:写 malloc 的时候,就把对应的 free 写好。
- 使用工具:Valgrind、AddressSanitizer 都能帮你检测泄漏。
10.3 双重释放:崩溃的元凶
双重释放,就是同一块内存被 free 了两次。后果很严重——轻则程序崩溃,重则被攻击者利用。
为什么会发生双重释放?最常见的原因是:多个指针指向同一块内存,然后各自 free 了一次。
int *p = (int *)malloc(sizeof(int));
int *q = p;
free(p);
free(q); // 双重释放!
解决方法是:free 之后立即把指针置为 NULL。这样第二次 free 时,free(NULL) 是安全的。
free(p);
p = NULL;
free(q); // q 还是指向原来的地址,但 p 已经是 NULL 了
// 不过 q 仍然有问题,所以最好也置 NULL
10.4 释放后使用(UAF):最隐蔽的 bug
UAF,全称 Use-After-Free。就是内存已经 free 了,但你还通过原来的指针去读写它。这是最隐蔽、最难调试的 bug 之一。
为什么难?因为 free 之后,那块内存可能已经被系统回收,也可能还在原地。你读写它,有时候能成功,有时候会崩溃,完全看运气。我见过一个 UAF 的 bug,在开发机上跑了一周都没复现,上线后客户那边一天崩三次。
典型的 UAF 场景:
int *p = (int *)malloc(sizeof(int));
*p = 42;
free(p);
// 这里 p 变成了悬空指针
*p = 100; // UAF!
避免 UAF 的方法:
- free 后立即置 NULL:这样后续的读写会立即崩溃,而不是随机崩溃。
- 使用智能指针:C 语言没有,但可以自己封装引用计数。
- 静态分析工具:Coverity、Clang Static Analyzer 能检测出部分 UAF。
10.5 知识体系图
下面这张图总结了动态内存管理的核心陷阱和规避方法。你可以把它当作一张“避坑地图”。
10.6 总结
动态内存管理,说白了就是三个字:责任心。你分配了,就要负责释放;你释放了,就要忘记它。别偷懒,别侥幸。
最后送你一句话:malloc 和 free 是夫妻,别让它们离婚。
公众号:蓝海资料掘金营,微信 deep3321