19、指针(五):动态内存分配(malloc/calloc/realloc/free)、内存泄漏的检测与预防
动态内存分配,说白了就是让程序在运行时向操作系统“要内存”。
你写代码时定义变量,那是静态分配——大小在编译时就定死了。但很多时候,你根本不知道用户会输入多少数据。比如一个文本编辑器,用户可能只写一行,也可能写一万行。这时候就需要动态分配。
我个人习惯把动态内存分配比作“借东西”。你跟系统借一块内存,用完了得还回去。不还?那就是内存泄漏。还错了?那就是野指针、段错误。嗯,这里面的门道不少。
19.1 四大内存操作函数
C语言标准库提供了四个核心函数:malloc、calloc、realloc 和 free。它们都声明在 <stdlib.h> 中。
19.1.1 malloc:申请一块未初始化的内存
void* malloc(size_t size);
malloc 分配 size 字节的内存,返回一个 void* 指针。注意:这块内存里的数据是随机的——也就是所谓的“垃圾值”。
我在项目中遇到过新手直接使用 malloc 返回的指针,结果读出来的数据莫名其妙。其实很简单,要么你自己用 memset 清零,要么直接用 calloc。
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理内存分配失败
fprintf(stderr, "内存分配失败\n");
exit(1);
}
19.1.2 calloc:申请一块已清零的内存
void* calloc(size_t num, size_t size);
calloc 分配 num 个元素,每个元素 size 字节,并且自动把这块内存全部初始化为 0。
我个人习惯在分配数组时优先用 calloc。为什么?因为清零能避免很多“未初始化”的坑。你想想看,如果 malloc 出来的内存里恰好有个非零值,你的逻辑可能就错了。
// 分配一个包含 100 个整数的数组,全部初始化为 0
int* arr = (int*)calloc(100, sizeof(int));
if (arr == NULL) {
// 处理错误
}
19.1.3 realloc:调整已分配内存的大小
void* realloc(void* ptr, size_t new_size);
realloc 用来改变之前分配的内存块的大小。它可能原地扩展,也可能搬到一个新地址。
我曾经踩过一个坑:直接用 p = realloc(p, new_size)。如果 realloc 失败,它会返回 NULL,但原来的内存块还在。这么写会把原来的指针弄丢,造成内存泄漏。
p = realloc(p, ...)。应该用一个临时变量。
int* new_p = (int*)realloc(p, new_size * sizeof(int));
if (new_p == NULL) {
// realloc 失败,原来的 p 仍然有效
// 这里要处理错误,但不要 free(p),因为 p 还在
fprintf(stderr, "realloc 失败\n");
} else {
p = new_p; // 只有成功时才更新指针
}
19.1.4 free:归还内存
void free(void* ptr);
free 把之前分配的内存还给系统。注意:只能 free 由 malloc/calloc/realloc 返回的指针。free 一个已经 free 过的指针,或者 free 一个栈上的指针,都是未定义行为。
free(p);
p = NULL; // 防止野指针
19.2 内存泄漏:看不见的杀手
内存泄漏是指程序分配了内存,但不再使用后没有释放。随着程序运行,泄漏的内存越来越多,最终可能导致系统内存耗尽。
你想想看,一个服务器程序跑上几个月,如果每天泄漏一点点,最后会怎样?嗯,崩溃。
19.2.1 常见的内存泄漏场景
- 忘记 free:分配了内存,但函数返回前忘了释放。
- 丢失指针:把指针重新赋值,原来的内存块再也找不到了。
- 异常路径:函数中有多个 return 点,有些路径忘了 free。
// 典型的内存泄漏
void func() {
int* p = (int*)malloc(100 * sizeof(int));
if (some_condition) {
return; // 忘了 free(p)!
}
// 使用 p
free(p);
}
19.2.2 如何检测内存泄漏
说实话,靠肉眼审查代码很难发现所有泄漏。我建议用工具。
| 工具 | 平台 | 说明 |
|---|---|---|
| Valgrind | Linux | 最常用的内存检测工具,能报告泄漏位置 |
| AddressSanitizer | GCC/Clang | 编译时加 -fsanitize=address,运行时检测 |
| CRT 调试堆 | Windows (MSVC) | 使用 _CrtDumpMemoryLeaks() 检测 |
19.3 内存泄漏的预防策略
预防永远比事后检测更省心。我总结了几条实用原则。
19.3.1 谁分配,谁释放
在函数设计时,明确约定:哪个函数分配了内存,就由哪个函数负责释放。如果函数返回一个动态分配的指针,文档里必须写清楚“调用者需要 free 返回值”。
19.3.2 使用 RAII 思想(C语言版)
C语言没有 RAII,但我们可以模仿。比如写一个结构体,把分配和释放封装成一对函数。
typedef struct {
int* data;
size_t size;
} DynamicArray;
DynamicArray* array_create(size_t size) {
DynamicArray* arr = (DynamicArray*)malloc(sizeof(DynamicArray));
if (arr == NULL) return NULL;
arr->data = (int*)calloc(size, sizeof(int));
if (arr->data == NULL) {
free(arr);
return NULL;
}
arr->size = size;
return arr;
}
void array_destroy(DynamicArray* arr) {
if (arr) {
free(arr->data);
free(arr);
}
}
19.3.3 编码规范
- 每个 malloc/calloc/realloc 都要有对应的 free。
- free 后立即置 NULL。
- 在函数开头就规划好所有退出路径,确保每条路径都释放了内存。
- 使用 goto 统一处理错误清理(C语言中常见的模式)。
// 使用 goto 统一清理
int* p1 = NULL;
int* p2 = NULL;
p1 = (int*)malloc(100 * sizeof(int));
if (p1 == NULL) goto cleanup;
p2 = (int*)malloc(200 * sizeof(int));
if (p2 == NULL) goto cleanup;
// 正常使用
// ...
cleanup:
free(p1);
free(p2);
return;
19.4 动态内存分配的核心流程图
下面这张图展示了动态内存分配与释放的完整生命周期,以及内存泄漏是如何发生的。
19.5 总结
动态内存分配是C语言的精髓,也是最容易出bug的地方。说白了,就三条原则:
- 申请了就要释放——每个 malloc/calloc/realloc 都要有对应的 free。
- 释放了就别再用——free 后置 NULL,防止野指针。
- 用工具辅助检查——Valgrind 和 AddressSanitizer 是你的好朋友。
我曾经在一个嵌入式项目里,因为一个内存泄漏导致设备运行三天后死机。查了两天才找到原因——一个函数里的 malloc 忘了 free。从那以后,我写代码时都会在函数开头就规划好所有退出路径,确保每条路径都释放了内存。
公众号:蓝海资料掘金营,微信deep3321