如果你写过 C/C++,一定对 malloc 和 free 不陌生。它们是动态内存管理的基石,但也是无数 Bug 的温床。很多内存漏洞并不是“写错了一行代码”,而是在逻辑上看起来合理,运行起来却埋下隐患。
这篇文章我们就来系统梳理 malloc / free 使用中的常见内存漏洞,配合代码示例,帮你避开那些“写着写着就踩了”的坑。
一、内存泄漏(Memory Leak)
1. 忘记 free
这是最经典、也最容易被忽视的问题。
void foo() {
int *p = (int *)malloc(sizeof(int) * 10);
// 使用 p ...
// 忘记 free(p)
}
问题:
每次调用 foo() 都会泄漏一块内存。如果这个函数被频繁调用(比如在循环或网络请求中),程序的内存占用会持续增长,最终可能被 OOM Killer 干掉。
正确做法:
void foo() {
int *p = (int *)malloc(sizeof(int) * 10);
if (!p) return;
// 使用 p ...
free(p);
p = NULL; // 防止悬空指针
}
2. 提前 return / goto 导致漏 free
int process() {
char *buf1 = malloc(1024);
char *buf2 = malloc(2048);
if (some_error) {
return -1; // buf1 和 buf2 都没释放!
}
free(buf1);
free(buf2);
return 0;
}
正确做法:使用统一的清理路径
int process() {
char *buf1 = NULL;
char *buf2 = NULL;
int ret = -1;
buf1 = malloc(1024);
buf2 = malloc(2048);
if (!buf1 || !buf2)
goto cleanup;
if (some_error)
goto cleanup;
ret = 0;
cleanup:
free(buf1);
free(buf2);
return ret;
}
二、悬空指针(Dangling Pointer)
free 后继续使用
int *p = malloc(sizeof(int));
*p = 10;
free(p);
*p = 20; // 未定义行为!
free 之后,那块内存已经归还给堆管理器,再访问就是未定义行为。可能表现为:
- 程序直接崩溃(SIGSEGV)
- 数据被其他变量覆盖,产生诡异 Bug
- 暂时“看起来正常”,但在特定条件下炸裂
正确做法:free 后置空
free(p);
p = NULL;
虽然不能防止所有错误,但至少后续对 NULL 的解引用会立刻崩溃,方便定位问题。
三、重复释放(Double Free)
char *p = malloc(100);
free(p);
free(p); // double free!
后果:
Double free 是堆破坏的经典来源,可能导致程序崩溃,也可能被攻击者利用来构造任意地址写入漏洞,是非常危险的安全问题。
正确做法:
free(p);
p = NULL;
// 后续再 free(p) 就是安全的,free(NULL) 是合法的空操作
四、越界访问(Buffer Overflow / Underflow)
写越界
int *arr = malloc(sizeof(int) * 5);
for (int i = 0; i <= 5; i++) { // i = 5 越界
arr[i] = i;
}
free(arr);
问题:
越界写入可能覆盖堆管理器的元数据(如 chunk header),导致后续 free 时崩溃,或者引发更难排查的堆破坏问题。
读越界
char *str = malloc(6);
strcpy(str, "hello");
char c = str[6]; // 越界读
读越界虽然不一定立刻崩溃,但可能读到脏数据,破坏程序逻辑。
五、不匹配的分配/释放
malloc / free 混用 new / delete
int *p = (int *)malloc(sizeof(int));
delete p; // 未定义行为
或反过来:
int *p = new int[10];
free(p); // 未定义行为
规则:
| 分配方式 | 释放方式 |
|---|---|
| malloc | free |
| new | delete |
| new[] | delete[] |
混用可能导致:
- 内存泄漏
- 析构函数不被调用(C++ 对象)
- 堆损坏
六、sizeof 的经典陷阱
对指针用 sizeof
int *arr = malloc(sizeof(arr) * 10); // 错误!
这里 sizeof(arr) 是指针的大小(通常 4 或 8 字节),而不是 int 的大小。
正确写法:
int *arr = malloc(sizeof(*arr) * 10);
// 或者
int *arr = malloc(sizeof(int) * 10);
推荐第一种写法,因为当类型改变时,不需要同步修改 malloc 中的类型。
七、内存未初始化
int *p = malloc(sizeof(int) * 10);
printf("%d\n", p[0]); // 未初始化,值是垃圾值
对比:
| 函数 | 是否初始化 |
|---|---|
| malloc | 否 |
| calloc | 是(清零) |
如果你需要清零的内存,用 calloc 更安全:
int *p = calloc(10, sizeof(int)); // 所有元素初始化为 0
八、realloc 的陷阱
1. 直接覆盖原指针
p = realloc(p, new_size); // 危险!
如果 realloc 失败,返回 NULL,而原来的 p 就丢了,造成内存泄漏。
正确做法:
void *new_p = realloc(p, new_size);
if (!new_p) {
// 处理错误,p 仍然有效
free(p);
return;
}
p = new_p;
2. realloc(0)
p = realloc(p, 0); // 行为是实现定义的
有些实现会返回 NULL,有些会释放内存并返回可 free 的指针。为了可移植性,避免这样写。
九、释放栈内存
int x;
int *p = &x;
free(p); // 未定义行为!
free 只能释放 malloc / calloc / realloc 返回的堆内存。释放栈变量、全局变量、字符串字面量都会出问题。
十、结构体中的指针成员
typedef struct {
char *name;
} Person;
Person *p = malloc(sizeof(Person));
p->name = malloc(64);
// ...
free(p); // name 指向的内存泄漏了!
正确做法:先释放成员,再释放结构体
free(p->name);
free(p);
十一、多线程下的内存问题
虽然 malloc / free 本身通常是线程安全的,但以下模式仍然危险:
// 线程 A
free(p);
// 线程 B
*p = 10; // 悬空指针
这类问题需要配合锁或智能指针(C++)来管理生命周期。
十二、防御性编程建议
1. 谁分配,谁释放
明确内存的所有权归属。如果 API 内部 malloc,文档应明确由调用者 free。
2. 使用工具检测
| 工具 | 用途 |
|---|---|
| Valgrind | 检测内存泄漏、越界、使用未初始化内存 |
| AddressSanitizer | 编译期插桩,检测堆/栈/全局越界 |
| Electric Fence | 捕获越界访问 |
3. C++ 优先使用智能指针
std::unique_ptr<int[]> arr(new int[10]);
// 或
auto arr = std::make_unique<int[]>(10);
RAII 可以彻底避免大多数手动 new/delete 的问题。
总结
malloc 和 free 看似简单,但坑无处不在。最常见的几类问题可以归纳为:
| 类型 | 关键词 |
|---|---|
| 泄漏 | 忘记 free、提前 return |
| 悬空 | free 后使用 |
| 重复释放 | double free |
| 越界 | 写/读越界 |
| 不匹配 | malloc/delete 混用 |
| 未初始化 | 垃圾值 |
| realloc | 覆盖原指针 |
一句话总结:
动态内存管理的核心不是“怎么分配”,而是 “谁拥有、谁释放、何时释放” 。把这三个问题想清楚,大部分坑都能提前避开。
