1. 内存泄漏基础:什么是内存泄漏、内存泄漏的危害、C++内存管理模型
什么是内存泄漏?
内存泄漏,说白了就是「借了东西不还」。
你向操作系统申请了一块内存,用完之后忘了释放。这块内存就一直被占用着,谁也拿不走。程序跑得越久,漏得越多,最后系统内存被吃光,程序就崩了。
我刚开始做C++开发时,觉得这玩意儿离我很远。直到有一次,我负责的一个后台服务跑了三天后突然挂掉,查了半天才发现是一个循环里每次new了对象却没delete。嗯,从那以后我再也不敢小看内存泄漏了。
核心定义:内存泄漏是指程序中已动态分配的堆内存,由于某种原因程序未释放或无法释放,造成系统内存的浪费。
一个简单的例子
void leakyFunction() {
int* p = new int(42); // 申请了内存
// 忘记 delete p;
// 函数返回后,p 丢失,内存再也无法释放
}
这个函数每调用一次,就泄漏4个字节。看起来不多?如果它在循环里被调用一万次呢?
内存泄漏的危害
你想想看,内存泄漏就像水管漏水。一开始只是滴滴答答,没人会在意。但时间长了,后果很严重。
- 程序变慢:内存越占越多,系统开始使用交换分区,性能断崖式下跌
- 程序崩溃:内存耗尽,new操作抛出std::bad_alloc异常
- 系统不稳定:一个程序把内存吃光了,其他程序也跟着遭殃
- 难以排查:泄漏往往在特定场景下才暴露,复现困难
我曾经踩过的坑:一个Qt界面程序,用户反馈说「用着用着就卡死了」。我远程看了下,任务管理器里内存占用从50MB一路涨到2GB。最后定位到是一个定时器里不断创建QWidget却不销毁。这种泄漏最隐蔽——它不报错,只是慢慢杀死你的程序。
C++内存管理模型
要理解内存泄漏,得先搞清楚C++的内存是怎么管的。我个人习惯把内存区域分成四个部分:
| 内存区域 | 存储内容 | 生命周期 | 常见问题 |
|---|---|---|---|
| 栈区 | 局部变量、函数参数 | 函数执行期间 | 栈溢出(递归太深) |
| 堆区 | 动态分配的对象 | 手动控制 | 内存泄漏、野指针 |
| 全局/静态区 | 全局变量、static变量 | 程序启动到结束 | 多线程竞争 |
| 代码区 | 程序指令 | 程序运行期间 | 一般没问题 |
内存泄漏主要发生在堆区。为什么呢?因为栈上的变量由编译器自动管理,函数结束就自动释放。而堆上的内存,你得自己管。
堆内存的生命周期
在C++里,堆内存的申请和释放是这样的:
// 申请
int* p = new int; // 单个对象
int* arr = new int[10]; // 对象数组
// 释放
delete p; // 释放单个对象
delete[] arr; // 释放对象数组
这里有个坑:new 和 delete 要配对,new[] 和 delete[] 要配对。混用会导致未定义行为。我见过有人用 delete 去释放 new[] 出来的数组,结果程序崩溃得一塌糊涂。
避坑指南:在Qt项目中,尽量使用Qt的父对象机制。把QObject对象的父对象设置好,父对象销毁时会自动销毁子对象。这能省去大量手动管理内存的麻烦。
为什么C++容易内存泄漏?
说白了,C++给了你最大的自由度,也给了你最大的责任。不像Java有垃圾回收,C++把内存管理的缰绳完全交到你手里。
常见的泄漏场景:
- 异常导致delete语句没执行到
- 指针被重新赋值,原来的内存没人管了
- 容器里存了指针,容器销毁时没遍历释放
- 循环引用导致智能指针无法释放
知识体系总览
下面这张图,是我整理的内存泄漏知识体系。你可以把它当作整个课程的地图:
小结
内存泄漏不是「会不会遇到」的问题,而是「什么时候遇到」的问题。每个C++开发者迟早都会栽跟头。区别在于,有的人栽一次就学会了,有的人反复栽。
我个人觉得,理解内存泄漏的关键就三点:
- 知道它是什么——借了内存没还
- 知道它怎么来的——堆内存管理不当
- 知道它怎么防——RAII、智能指针、Qt父对象机制
后面的章节,我会带着你一步步把这些工具用起来。嗯,咱们慢慢来。
一个小建议:从现在开始,每次写new的时候,脑子里自动弹出「我什么时候delete它?」这个问题。养成这个习惯,能帮你避开80%的内存泄漏。