C++中的内存泄漏和损坏
这是困扰程序员的两个重大问题,其本质都是因为:指针。
指针
我们都知道,指针是“保存某块内存地址的变量”。
1 | int a = 10; |
通过 *p 可以访问这块地址中的内容:
1 | cout << *p << endl; // 10 |
为什么指针危险?
因为指针只知道“地址”,它并不天然知道:这块内存是不是还有效、这块内存有多大、该不该由你释放、你是不是越界了,所以一旦管理不当,就会产生各种 bug。
为什么会出问题?
C++ 很重视“对象的生命周期”。比如:
1 | int* p = new int(5); |
如果销毁后还使用:cout << *p << endl; 这是未定义的行为,是典型的危险场景,因为:指针还在,不代表对象还在。
什么是内存泄漏?
定义:申请了内存,但后来再也没有办法释放它。
最简单例子:
1 | void f() { |
问题在哪里?
new int(100) 在堆上申请了一块内存,函数结束时,局部变量 p 被销毁,但那块堆内存没人再能找到,于是这块内存泄漏了。
正确做法:在最后加一句delete p;
数组泄漏:
1 | int* arr = new int[100]; |
常见泄漏场景
- 忘记
delete同上 - 指针被覆盖
1 | int* p = new int(10); |
第一次 new 出来的内存泄漏了
- 异常导致没走到释放代码
1 | void f() { |
- 容器里放裸指针但没清理
1 | vector<int*> v; |
如果程序结束前没逐个 delete,就泄漏。
- 循环里不断分配
1 | while (true) { |
这会持续吃内存,最终可能把系统拖垮。
什么是内存损坏(Memory Corruption)
“内存泄漏”是没释放。
“内存损坏”是把不该写的地方写坏了,或者把内存管理状态弄乱了。
这是更危险的一类问题,因为它往往会:随机崩溃、表现不稳定、在别处报错、很难定位。
常见的内存损坏
- 越界访问(buffer overflow / out-of-bounds)
1 | int* arr = new int[3]; |
arr[3] 已经超出合法范围。后果可能是:改坏相邻变量、改坏堆管理器元数据、之后 delete[] 时崩溃。
- 释放后使用(use-after-free)
1 | int* p = new int(10); |
这块内存已经不属于你了,再写就是未定义行为。
- 重复释放(double free)
1 | int* p = new int(10); |
第一次释放后,这块内存已经回收了。
第二次再释放,会破坏堆分配器的内部状态。
- 释放方式不对
1 | int* arr = new int[10]; |
或者
1 | int x; |
- 野指针(wild pointer)
1 | int* p; |
这是非常危险的。
- 悬空指针(dangling pointer)
1 | int* p = new int(5); |
p 就成了悬空指针。
常见缓解手法:
1 | delete p; |
这样再误用时更容易暴露问题。
为什么内存损坏经常“不是当场炸”
例如:
1 | int* arr = new int[3]; |
你以为会在 arr[100] = 1 这里马上崩。
但实际可能:当场没事、过几分钟才崩、在完全无关的地方崩、只在某台机器上崩
原因是:
C++ 对这些错误大多属于“未定义行为(Undefined Behavior)”。
意思是标准不保证任何结果。
你可能改坏的是:其他变量、堆分配器管理信息、虚函数表、返回地址附近内容(某些情况下)、所以症状常常延后出现。
未定义行为(UB)
C++ 里内存错误大多都属于 UB。
典型 UB 包括:越界访问、释放后使用、重复释放、解引用空指针、解引用未初始化指针
UB 的含义不是“结果随机”这么简单,而是:
- 编译器可以做任何事
- 程序可能看起来正常
- 也可能崩溃
- 也可能产生隐蔽错误
所以不能靠“我跑了一下没问题”来证明代码安全