malloc 与 free 的那些坑:C/C++ 内存管理的常见漏洞与踩坑实录

举报
yd_249398798 发表于 2026/09/04 16:03:02 2026/09/04
【摘要】 malloc 与 free 的那些坑:C/C++ 内存管理的常见漏洞与踩坑实录如果你写过 C/C++,一定对 malloc 和 free 不陌生。它们是动态内存管理的基石,但也是无数 Bug 的温床。很多内存漏洞并不是“写错了一行代码”,而是在逻辑上看起来合理,运行起来却埋下隐患。这篇文章我们就来系统梳理 malloc / free 使用中的常见内存漏洞,配合代码示例,帮你避开那些“写着写着...

malloc 与 free 的那些坑:C/C++ 内存管理的常见漏洞与踩坑实录

如果你写过 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
覆盖原指针
一句话总结:​
动态内存管理的核心不是“怎么分配”,而是“谁拥有、谁释放、何时释放”。把这三个问题想清楚,大部分坑都能提前避开。



【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。