第22章:动态内存管理——malloc、calloc、realloc、free的使用,内存泄漏的避免
说实话,动态内存管理是C语言里最容易翻车的地方之一。我见过太多项目,明明逻辑没问题,跑着跑着就崩了——查到最后,十有八九是内存出了问题。
你想想看,静态数组的大小是编译时就定死的。但实际开发中,你往往要到运行时才知道需要多少内存。比如你要读一个文件,文件多大?不知道。这时候就需要动态内存管理了。
嗯,这一章我们就把它彻底讲透。
一、四个核心函数
C语言标准库提供了四个函数来管理堆内存。它们都声明在 <stdlib.h> 里。
1. malloc —— 申请一块内存
void *malloc(size_t size);
malloc 分配 size 字节的内存。它不会初始化这块内存——里面的数据是随机的。我个人习惯,拿到 malloc 返回的指针后,如果马上要用,就手动 memset 清零。
关键点:malloc 返回的是 void*,需要强制转换成目标类型。分配失败返回 NULL。
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理分配失败
}
2. calloc —— 申请并清零
void *calloc(size_t nmemb, size_t size);
calloc 分配 nmemb 个元素,每个元素 size 字节。它会把分配的内存全部初始化为 0。
我在项目中遇到过一个问题:用 malloc 分配了一个结构体数组,结果某个成员没初始化,导致后续逻辑判断出错。后来改用 calloc,问题就解决了。
int *p = (int *)calloc(10, sizeof(int));
// 这10个int全部是0
小技巧:如果你需要清零的内存,直接用 calloc 比 malloc + memset 更高效。因为 calloc 在底层可能利用操作系统的零页优化。
3. realloc —— 调整内存大小
void *realloc(void *ptr, size_t size);
realloc 用来调整之前分配的内存块大小。它可能原地扩展,也可能搬到一个新地址。
这里有个坑,我曾经踩过:
// 错误用法
int *p = (int *)malloc(10 * sizeof(int));
p = (int *)realloc(p, 20 * sizeof(int)); // 如果realloc失败,返回NULL,原来的内存就丢了!
正确的做法是:
int *p = (int *)malloc(10 * sizeof(int));
int *tmp = (int *)realloc(p, 20 * sizeof(int));
if (tmp != NULL) {
p = tmp; // 只有成功才赋值
} else {
// 处理失败,p仍然有效
}
4. free —— 释放内存
void free(void *ptr);
free 把内存还给系统。注意:只能释放 malloc/calloc/realloc 返回的指针。不能释放栈上的地址,也不能重复释放。
警告:free 之后一定要把指针置为 NULL。否则就成了野指针,后续误用会出大问题。
free(p);
p = NULL; // 好习惯
二、内存泄漏——看不见的杀手
内存泄漏是什么?说白了就是你申请了内存,用完了没释放。程序跑得越久,占用的内存越多,最后系统扛不住了。
我见过最离谱的一次:一个嵌入式设备跑了三天后死机,查了整整一周才发现是一个循环里每次都要 malloc,但只在特定条件下才 free。条件不满足时,内存就漏了。
常见泄漏场景
- 忘记 free:最直接的泄漏
- 丢失指针:把指针重新赋值了,原来的内存再也找不回来
- 异常路径没释放:函数中间 return 了,free 代码没执行到
- 结构体嵌套:释放了外层,忘了释放内层指针指向的内存
三、避坑指南
这些都是我用真金白银换来的教训:
我曾经...在一个通信协议栈里,为了省事,把 malloc 和 free 分散在不同的模块里。结果调试了整整两天,才发现是某个模块在释放后没有置 NULL,另一个模块又去访问了。
从那以后,我给自己定了几个规矩:
- 谁申请谁释放——尽量让同一个函数或模块负责 malloc 和 free
- free 后立即置 NULL——这是肌肉记忆
- 检查返回值——malloc/calloc/realloc 都可能失败,尤其是嵌入式环境
- 用工具检测——Valgrind 是 Linux 下的神器,嵌入式可以用静态分析工具
四、知识体系图
下面这张图把动态内存管理的核心逻辑串起来了:
五、完整示例
最后,给你看一个实际项目中常用的模式——动态数组:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;
// 初始化
int da_init(DynamicArray *arr, size_t cap) {
arr->data = (int *)malloc(cap * sizeof(int));
if (arr->data == NULL) return -1;
arr->size = 0;
arr->capacity = cap;
return 0;
}
// 添加元素
int da_push(DynamicArray *arr, int val) {
if (arr->size >= arr->capacity) {
// 扩容:每次翻倍
size_t new_cap = arr->capacity * 2;
int *tmp = (int *)realloc(arr->data, new_cap * sizeof(int));
if (tmp == NULL) return -1;
arr->data = tmp;
arr->capacity = new_cap;
}
arr->data[arr->size++] = val;
return 0;
}
// 释放
void da_free(DynamicArray *arr) {
free(arr->data);
arr->data = NULL;
arr->size = 0;
arr->capacity = 0;
}
int main() {
DynamicArray arr;
if (da_init(&arr, 4) != 0) {
printf("初始化失败\n");
return 1;
}
for (int i = 0; i < 10; i++) {
da_push(&arr, i * i);
}
for (size_t i = 0; i < arr.size; i++) {
printf("%d ", arr.data[i]);
}
printf("\n");
da_free(&arr);
return 0;
}
这个模式我用了很多年。它把内存管理的细节封装在几个函数里,外面调用的人不用操心 malloc 和 free 的细节。说白了,就是降低心智负担。
最后说一句:动态内存管理没有银弹。你只能靠良好的编码习惯和工具辅助来减少问题。我个人的经验是——写代码时多花一分钟想清楚释放路径,能省下后面十小时的调试时间。
公众号:蓝海资料掘金营,微信deep3321