3. 堆内存管理
堆内存,说白了就是程序运行时动态申请的那块地儿。跟栈上自动分配回收不同,堆上的内存你得自己管——申请、使用、释放,少一步都不行。C标准库给了我们四个函数来处理这件事:malloc、calloc、realloc 和 free。今天咱们就把它们掰开揉碎了讲清楚。
3.1 malloc:最基础的堆内存申请
malloc 的全称是 memory allocation。它的原型很简单:
void *malloc(size_t size);
传入你要的字节数,返回一个指向这块内存的指针。如果失败,返回 NULL。
我个人习惯,每次 malloc 之后都会检查返回值。你想想看,嵌入式设备上内存本来就紧张,申请失败是常有的事。不检查就直接用,解引用空指针,程序直接就崩了。
核心要点:malloc 分配的内存是未初始化的。里面的数据是随机值,可能是上次使用留下的垃圾数据。
#include <stdlib.h>
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理内存分配失败
return -1;
}
// 使用 p 指向的内存
// ...
free(p);
这里有个细节:sizeof(int) 在不同平台上可能是 2 字节或 4 字节。我建议永远用 sizeof 来计算大小,别写死数字。我在项目中遇到过有人写 malloc(40),结果换了个平台就出问题了。
3.2 calloc:带初始化的内存申请
calloc 跟 malloc 很像,但有两个区别:
- 它接受两个参数:元素个数和每个元素的大小
- 它会自动把分配的内存清零
void *calloc(size_t nmemb, size_t size);
举个例子:
int *arr = (int *)calloc(10, sizeof(int));
// 此时 arr[0] 到 arr[9] 全部是 0
为什么会有 calloc?说白了就是省事。如果你需要一块初始化为零的内存,用 calloc 一步到位。用 malloc 的话你还得自己写个 memset。
小技巧:calloc 内部会做乘法 nmemb * size,如果乘积溢出,它会返回 NULL。而 malloc 不会帮你检查这个。所以分配数组时,用 calloc 更安全。
不过要注意,calloc 的清零操作是有开销的。如果你不需要零初始化,用 malloc 更快。我曾经在一个音视频处理项目里,用 calloc 分配了 100MB 的缓冲区,结果初始化花了十几毫秒,后来改成 malloc 就快多了。
3.3 realloc:动态调整内存大小
有时候你申请的内存不够用了,或者太多了想缩一缩。这时候 realloc 就派上用场了。
void *realloc(void *ptr, size_t new_size);
它的行为有点复杂,我总结一下:
- 如果
ptr是 NULL,等价于malloc(new_size) - 如果
new_size是 0,等价于free(ptr) - 如果新大小比原来大,新增加的部分是未初始化的
- 如果新大小比原来小,多余的部分会被丢弃
嗯,这里有个坑。realloc 可能会移动内存块,返回一个新的地址。原来的指针就失效了。所以一定要用返回值:
int *p = (int *)malloc(10 * sizeof(int));
// ... 使用 p ...
int *new_p = (int *)realloc(p, 20 * sizeof(int));
if (new_p == NULL) {
// realloc 失败,原来的 p 仍然有效
// 但要注意处理
} else {
p = new_p; // 更新指针
}
警告:千万不要写成 p = realloc(p, new_size)!如果 realloc 失败返回 NULL,原来的 p 就丢了,造成内存泄漏。
我在项目中遇到过这种情况:一个动态数组不断增长,每次 realloc 都复制数据,性能很差。后来我改成每次翻倍增长,减少了 realloc 调用次数。说白了就是空间换时间。
3.4 free:释放内存,别忘了我
申请了就要释放,这是铁律。free 的原型:
void free(void *ptr);
释放后,指针变成野指针。我建议立即把指针置为 NULL:
free(p);
p = NULL;
为什么要这么做?因为 free 之后 p 指向的地址已经无效了,但 p 本身的值还在。如果不置 NULL,后面不小心又用了一次 free(p),那就是 double free,程序会崩溃。
我曾经在一个通信协议栈项目里,排查了一整天的 double free 问题。最后发现是某个回调函数里释放了指针,但主循环里又释放了一次。从那以后,我养成了 free 后立即置 NULL 的习惯。
3.5 常见错误与避坑指南
堆内存管理最容易出问题的几个地方:
- 内存泄漏:申请了没释放。程序跑久了内存越占越多,最后 OOM。
- 野指针:释放后继续使用。这是未定义行为,可能当场崩溃,也可能过一会儿才崩。
- 越界访问:写入了超出申请范围的数据。会破坏堆管理结构,导致 free 时崩溃。
- 重复释放:对同一个指针 free 两次。
避坑指南:我曾经在一个嵌入式项目里,malloc 和 free 配对使用,但有个分支路径忘了 free,导致设备运行 72 小时后内存耗尽重启。排查时用了一个简单的办法:在 malloc 和 free 处加日志,统计申请和释放的次数。一对比就发现问题了。
3.6 知识体系图
下面这张图展示了堆内存管理的核心逻辑:
3.7 总结
堆内存管理说白了就三件事:申请、使用、释放。每个环节都有坑,但记住几条原则就能避开大部分问题:
- malloc 后检查 NULL
- calloc 适合需要清零的场景
- realloc 用返回值更新指针
- free 后立即置 NULL
- 申请和释放要成对出现
嗯,这些经验都是我用实际加班换来的。你想想看,一个内存泄漏查三天,还不如一开始就养成好习惯。
我的建议:在嵌入式开发中,尽量少用动态内存分配。如果非用不可,一定要有配套的释放机制。我见过太多因为堆管理不当导致的死机问题了。
可可精品资料库