1. 内存泄漏基础:什么是内存泄漏、内存泄漏的危害、常见的内存泄漏场景
1.1 到底什么是内存泄漏?
说白了,内存泄漏就是「借了不还」。
你向操作系统申请了一块内存,用完之后忘了释放。这块内存就永远「悬」在那里,谁也碰不了。我刚开始学C++时,觉得这没什么大不了的——不就是几KB吗?后来我才明白,这玩意儿就像厨房水龙头没关紧,一滴一滴地漏,一晚上就能把你家给淹了。
严格来说,内存泄漏是指:动态分配的内存(通过 new/malloc 获得)失去所有引用,导致无法被释放,直到程序结束才归还给操作系统。
核心特征:
- 内存还在,但指针丢了
- 程序无法访问,也无法释放
- 进程不退出,内存就永远不归还
举个例子:
void leak_example() {
int* p = new int(42); // 申请了4字节
// 忘记 delete p;
// 函数结束,p 销毁了,但内存还在
// 这块内存再也找不回来了
}
嗯,这里要注意:p 是栈上的局部变量,函数结束就自动销毁了。但 new 出来的内存是在堆上,没人管它。它就成了「孤儿内存」。
1.2 内存泄漏的危害有多大?
你可能觉得,漏几KB没什么。但我在项目中见过一个真实案例:一个后台服务跑了三天,内存从 200MB 涨到 8GB,最后被 OOM Killer 直接干掉了。整个线上业务挂了15分钟,老板脸都绿了。
内存泄漏的危害,我总结为四点:
| 危害 | 具体表现 | 严重程度 |
|---|---|---|
| 性能下降 | 内存碎片化,分配变慢,CPU缓存命中率降低 | ★★★ |
| 系统崩溃 | 内存耗尽,进程被 OOM Killer 杀死 | ★★★★★ |
| 资源浪费 | 其他进程拿不到内存,整体吞吐量下降 | ★★★ |
| 排查困难 | 泄漏往往在特定路径下才触发,复现难 | ★★★★ |
我曾经踩过的坑:
一个嵌入式设备,内存只有 64MB。有个模块每次处理请求泄漏 1KB,看起来不多。但设备要连续运行几个月。你算算:1KB × 1000次/天 × 30天 ≈ 30MB。两个月后设备就死机了。客户投诉说「你们的设备用着用着就死机」,排查了整整一周才找到原因。
1.3 常见的内存泄漏场景
我这些年见过的泄漏场景,归纳起来无非这几种。你想想看,是不是都遇到过?
场景一:忘记释放
最基础的情况,就是忘了写 delete 或 free。
void process() {
char* buf = new char[1024];
// 处理数据...
// 忘了 delete[] buf;
}
这种问题在代码审查时很容易漏掉。我个人习惯是:写 new 的同时就写 delete,就像拿钥匙的同时就想着还钥匙。
场景二:异常路径导致泄漏
这个更隐蔽。函数中途抛异常了,释放代码没执行到。
void risky_function() {
int* data = new int[100];
// 中间某处抛出异常
do_something_risky(); // 可能抛异常!
delete[] data; // 这行永远不会执行到
}
为什么会这样?因为异常会直接跳转到 catch 块,delete 被跳过了。我建议用 RAII 或者智能指针来避免这种问题。
场景三:容器中存储裸指针
很多人喜欢用 vector<int*> 或者 map<string, Object*>。清空容器时,只清空了指针,没释放指向的对象。
std::vector<int*> vec;
vec.push_back(new int(1));
vec.push_back(new int(2));
vec.clear(); // 只清空了指针,两个 int 泄漏了
嗯,这里要注意:clear() 只销毁容器内的元素(也就是指针本身),不会去调用 delete。你得手动遍历释放,或者直接用 vector<unique_ptr<int>>。
场景四:循环引用(智能指针的坑)
用了 shared_ptr 就万事大吉了?不是的。循环引用会让引用计数永远不为零。
struct Node {
std::shared_ptr<Node> next;
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 互相引用,谁也释放不了
说白了,就是两个人互相指着对方说「你先走」,结果谁都不走。解决办法是用 weak_ptr 打破循环。
场景五:全局/静态容器不断增长
这个我印象特别深。有个全局的 std::list,每次请求都往里塞日志对象,但从来没清理过。程序跑得越久,内存越大。
// 全局变量
std::list<LogEntry> g_logs;
void on_request() {
LogEntry entry = parse_request();
g_logs.push_back(entry); // 只增不减
}
这种泄漏最隐蔽——代码逻辑上「没错」,但设计上就是错的。你得问自己:这个容器有没有上限?需不需要淘汰机制?
1.4 知识体系总览
下面这张图,是我梳理的内存泄漏知识框架。你可以把它当作本章的「地图」:
我的建议:
刚开始学内存泄漏,别急着上工具。先把这五种场景记在心里。写代码时多问自己一句:「这块内存谁负责释放?」养成这个习惯,能避免80%的泄漏问题。
好了,这一章就到这里。记住一句话:内存泄漏不是 bug,是习惯问题。下一章我们聊聊怎么用工具把这些泄漏揪出来。
可可精品资料库