3. 动态内存管理:malloc/free与new/delete,内存泄漏的检测,悬空指针的产生
动态内存管理,说白了就是程序运行时跟操作系统“要内存”和“还内存”。
这事儿看着简单,但坑特别多。我见过太多线上事故,最后查下来都是内存管理出了问题。今天咱们就把这块掰开揉碎了讲清楚。
3.1 malloc/free 与 new/delete 的区别
很多新手觉得这两组东西差不多,其实差别大了去了。
| 对比项 | malloc/free | new/delete |
|---|---|---|
| 语言 | C 标准库函数 | C++ 运算符 |
| 返回值 | void*,需要强制类型转换 | 自动返回正确类型指针 |
| 调用构造函数 | 不会 | 会 |
| 调用析构函数 | 不会 | 会 |
| 失败处理 | 返回 NULL | 抛出 std::bad_alloc 异常 |
| 内存大小 | 需要手动计算字节数 | 编译器自动计算 |
我个人习惯是:C++ 项目里只用 new/delete。为什么?因为 malloc 不会调用构造函数,你想想看,一个对象只分配了内存却没初始化,后面用起来全是未定义行为。
核心原则:malloc/free 和 new/delete 绝对不能混用。你用 new 分配的内存,必须用 delete 释放;用 malloc 分配的,必须用 free 释放。混着用,轻则内存泄漏,重则程序崩溃。
// 错误示范:混用
int* p = (int*)malloc(sizeof(int));
delete p; // 未定义行为!
3.2 内存泄漏的检测
内存泄漏是什么?就是你申请了内存,用完了没还回去。一次两次没事,但程序跑久了,内存越占越多,最后系统扛不住就挂了。
我在项目中遇到过最离谱的一次:一个后台服务跑了三天,内存从 200MB 涨到了 8GB。查了两天才发现是一个循环里 new 了对象,但只在特定条件下才 delete。
3.2.1 手动检测方法
- 代码审查:检查每个 new 是否有对应的 delete,每个 malloc 是否有对应的 free
- 重载 new/delete:自己写一个内存分配计数器,记录分配和释放的次数
- 智能指针:用 std::unique_ptr 或 std::shared_ptr 替代裸指针
小技巧:我写代码有个习惯——每次 new 完,立刻写对应的 delete。哪怕后面要改逻辑,delete 的位置也先占着。这叫“先还再借”,能避免很多遗漏。
3.2.2 工具检测
手动查太累了,咱们用工具。
| 工具 | 平台 | 特点 |
|---|---|---|
| Valgrind | Linux | 功能强大,能检测内存泄漏、越界、未初始化变量 |
| AddressSanitizer | Linux/macOS | 编译时插桩,速度快,适合集成到 CI |
| Dr. Memory | Windows/Linux | 类似 Valgrind,对 Windows 支持更好 |
| Visual Studio 诊断工具 | Windows | 集成在 IDE 里,用起来方便 |
// 用 AddressSanitizer 检测内存泄漏
// 编译时加上 -fsanitize=address -g
// 运行程序,如果有泄漏,会打印出详细的调用栈
#include <stdlib.h>
void leak() {
int* p = (int*)malloc(sizeof(int) * 100);
// 忘记 free(p)
}
int main() {
leak();
return 0;
}
// 运行后输出:
// ==12345==ERROR: LeakSanitizer: detected memory leaks
// Direct leak of 400 byte(s) in 1 object(s) allocated from:
// #0 0x7f... in malloc
// #1 0x... in leak() /path/to/file.cpp:5
注意:AddressSanitizer 会显著增加内存占用(约 2-3 倍),不适合在生产环境开启。但开发和测试阶段一定要用。
3.3 悬空指针的产生
悬空指针,就是指针指向的内存已经被释放了,但指针本身还在用。这是 C/C++ 里最危险的 bug 之一。
为什么会这样?说白了就是“人走了,钥匙还在你手里”。你拿着钥匙去开门,发现门已经拆了——程序就崩了。
3.3.1 常见的悬空指针场景
- 释放后未置空:delete 之后指针还保存着原来的地址
- 函数返回局部变量地址:局部变量在函数返回后就被销毁了
- 多个指针指向同一块内存:一个指针释放了内存,其他指针不知道
- 容器元素被删除:迭代器或指针还指向已经被删除的元素
// 场景1:释放后未置空
int* p = new int(42);
delete p;
*p = 10; // 悬空指针!未定义行为
// 场景2:返回局部变量地址
int* getLocal() {
int x = 10;
return &x; // x 在函数返回后就不存在了
}
int* p = getLocal();
cout << *p; // 悬空指针!
// 场景3:多个指针指向同一块内存
int* p1 = new int(100);
int* p2 = p1;
delete p1;
p1 = nullptr;
*p2 = 200; // 悬空指针!p2 指向的内存已经被释放
3.3.2 如何避免悬空指针
- 释放后立即置空:delete 之后马上把指针设为 nullptr
- 使用智能指针:std::shared_ptr 和 std::weak_ptr 能自动管理生命周期
- 明确所有权:谁分配谁释放,不要多个模块同时持有裸指针
- RAII 思想:资源获取即初始化,让对象的析构函数自动释放资源
避坑指南:我曾经在一个多线程项目里吃过亏。线程 A 释放了内存,线程 B 还在用那个指针。查了两天才发现是竞态条件导致的悬空指针。后来我强制团队规定:所有跨线程传递的指针必须用 shared_ptr,谁都不准用裸指针。
3.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你看一眼就能明白:动态内存管理其实就三件事——怎么分配、怎么释放、怎么避免踩坑。
嗯,动态内存管理这块内容就这些。记住一句话:谁分配谁释放,释放后即置空。能做到这点的,基本不会出大问题。
总结:
- malloc/free 是 C 的方式,new/delete 是 C++ 的方式,别混用
- 内存泄漏用 Valgrind 或 AddressSanitizer 检测,省时省力
- 悬空指针最危险,释放后置空 + 智能指针是王道
- 多线程环境下,用 shared_ptr 替代裸指针传递所有权
可可精品资料库