4. 动态内存分配:malloc/calloc/realloc/free 的正确用法与内存分配失败处理
动态内存分配,说白了就是程序运行时,跟操作系统“要一块地”来用。这块地有多大、什么时候还回去,都得你自己说了算。C 语言里,malloc、calloc、realloc 和 free 就是干这个活的四个兄弟。
我刚开始学的时候,觉得这玩意儿挺简单——要内存就调函数,用完就释放。结果呢?项目一复杂,各种野指针、内存泄漏、double free 全来了。嗯,今天咱们就把这四个兄弟的脾气摸透。
4.1 malloc:最基础的“要地”方式
malloc 的全称是 memory allocation。它从堆上申请一块连续的内存,返回一个 void* 指针。注意,它不会初始化这块内存——里面是啥垃圾数据,完全看运气。
#include <stdlib.h>
int *p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理分配失败
fprintf(stderr, "内存分配失败\n");
exit(1);
}
我个人习惯,每次 malloc 之后立刻检查返回值。为什么?因为一旦 p 是 NULL,你后面直接写 p[0] = 42,那就是对空指针解引用——程序直接崩给你看。
4.2 calloc:带初始化的“要地”方式
calloc 和 malloc 的区别就两点:第一,它会自动把申请的内存全部清零;第二,它的参数是两个——元素个数和每个元素的大小。
int *arr = (int*)calloc(10, sizeof(int));
// 等价于 malloc(10 * sizeof(int)) 然后 memset(arr, 0, 10 * sizeof(int))
我在项目中遇到过一个问题:用 malloc 申请了一个结构体数组,结果某个字段没初始化,导致后续逻辑判断出错。排查了半天,最后发现是垃圾值惹的祸。从那以后,如果申请的内存需要初始化为零,我直接上 calloc,省心。
4.3 realloc:动态调整内存大小
有时候,你一开始申请的内存不够用了,或者太多了想缩一缩。realloc 就是干这个的。它可以在原有内存块的基础上调整大小,尽量不移动数据。
int *p = (int*)malloc(5 * sizeof(int));
// ... 用着用着发现不够了
int *new_p = (int*)realloc(p, 10 * sizeof(int));
if (new_p == NULL) {
// realloc 失败,原内存块 p 仍然有效
fprintf(stderr, "realloc 失败\n");
free(p);
exit(1);
}
p = new_p;
这里有个坑,我曾经踩过:realloc 的第一个参数可以是 NULL。如果传 NULL,它的行为就和 malloc 一样。但千万别传一个已经被 free 掉的指针——那是未定义行为,后果自负。
4.4 free:用完了一定要还
申请了内存,用完了就得还回去。不还?那就是内存泄漏。程序跑得越久,泄漏越多,最后系统内存耗尽,程序被干掉。
int *p = (int*)malloc(100 * sizeof(int));
// 使用 p
free(p);
p = NULL; // 好习惯:释放后置空
我见过最典型的错误:free 之后没有把指针置 NULL,然后后面不小心又 free 了一次——double free,程序直接 abort。所以,我每次 free 之后,立刻把指针赋为 NULL。这样即使不小心再 free,free(NULL) 是安全的。
4.5 内存分配失败处理
动态内存分配不是百分百成功的。系统内存不足、堆空间碎片化、申请大小超过限制——都可能导致分配失败。这时候,malloc、calloc、realloc 都会返回 NULL。
怎么处理?我一般分两种情况:
- 小型分配失败:直接报错退出,或者尝试释放一些缓存再重试。
- 大型分配失败:记录日志,优雅地释放已有资源,然后退出。
void *safe_malloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "错误:无法分配 %zu 字节内存\n", size);
// 可以在这里尝试释放缓存
// 或者直接退出
exit(EXIT_FAILURE);
}
return ptr;
}
我个人习惯写一个 safe_malloc 这样的包装函数,把判空逻辑封装起来。这样主代码里就不用到处写 if 判空了,清爽很多。
4.6 知识体系总览
下面这张图,把四个函数的关系和核心要点串起来了。你一看就明白:
4.7 常见错误与避坑指南
说一千道一万,不如看看实际中容易犯的错。我整理了几个高频问题:
| 错误类型 | 表现 | 解决方法 |
|---|---|---|
| 内存泄漏 | 程序内存持续增长,最终 OOM | 每次 malloc 对应一个 free,用工具如 Valgrind 检测 |
| 野指针 | 访问已释放的内存,数据错乱或崩溃 | free 后立即置 NULL,避免悬空指针 |
| 越界访问 | 写入了申请范围之外的内存,破坏堆结构 | 严格检查索引范围,用 sizeof 计算大小 |
| double free | 程序 abort,core dump | free 后置 NULL,避免重复释放 |
| 忘记判空 | 对 NULL 指针解引用,段错误 | 每次分配后立即检查返回值 |
嗯,动态内存分配这块,说白了就是“借要还,借多少记清楚,还了就别再碰”。你只要记住这三点,大部分坑都能避开。至于那些更隐蔽的问题——比如堆内存碎片化、realloc 的移动开销——等你项目做深了,自然会遇到。到时候再回来翻翻这节课,应该会有新的体会。
可可精品资料库