26. 内存泄漏常见模式:忘记delete、异常安全

聊到内存泄漏,我脑子里立刻浮现出刚入行时的一段经历。那时候我写了一个图像处理模块,跑了一整天测试,结果内存占用从200MB一路飙到2GB,最后系统直接OOM了。排查了半天,发现就是少写了一个delete[]。嗯,这种错误,说白了就是「挖了坑忘了填」。

今天咱们就专门聊聊两种最常见的泄漏模式:忘记delete异常安全。你想想看,这两个问题几乎覆盖了90%以上的手动内存管理事故现场。

模式一:忘记delete——最朴素的坑

先看一个最直接的例子:

void processData() {
    int* buffer = new int[1024];
    // ... 处理数据 ...
    // 忘记 delete[] buffer;
}

这种代码,每次调用都会泄漏1024个int。跑一次两次没事,跑一万次呢?内存就炸了。

我个人习惯把这种泄漏分成三类:

泄漏类型 典型场景 后果
直接遗漏 new后忘记写delete 每次调用泄漏固定大小内存
分支遗漏 某些return路径没释放 特定条件下泄漏
覆盖遗漏 指针被重新赋值前没释放旧内存 旧内存永远丢失

分支遗漏特别坑。我在项目中遇到过这样的代码:

void handleRequest(const std::string& input) {
    char* temp = new char[input.size() + 1];
    strcpy(temp, input.c_str());

    if (input.empty()) {
        // 直接返回,忘记释放temp
        return;
    }

    // ... 处理 ...
    delete[] temp;
}

你看,input.empty()这个分支,temp就泄漏了。这种bug在代码审查时很容易漏掉,因为「正常路径」看起来是对的。

⚠️ 警告: 分支路径上的delete遗漏是最隐蔽的泄漏。建议每次写new时,立刻补上对应的delete,就像「先系安全带再点火」。

模式二:异常安全——C++特有的陷阱

这个模式,说白了就是「代码还没跑到delete,异常先来了」。C++的异常机制让内存管理变得复杂了很多。

看这个例子:

void riskyFunction() {
    Resource* res = new Resource();
    doSomethingThatMayThrow();  // 如果这里抛异常
    delete res;                 // 这行永远不会执行
}

为什么会这样?因为C++的异常会沿着调用栈向上传播,delete res直接被跳过了。res指向的内存就成了孤儿。

我曾经在一个交易系统里遇到过这种问题。某个网络请求超时抛了异常,结果导致一批订单对象的内存泄漏。系统跑了三天,内存占用从正常值翻了三倍。排查时发现,每个异常路径都漏了delete。

解决异常安全问题的经典方案有三个:

  1. RAII(资源获取即初始化)——把资源绑定到栈对象的生命周期上
  2. 智能指针——让析构函数自动释放
  3. try-catch-finally——手动保证释放(不推荐,容易遗漏)

我个人最推荐RAII。你想想看,栈对象在异常发生时一定会被析构,这是C++给我们的保证。利用这个特性,就能从根本上避免异常安全导致的泄漏。

💡 核心原则: 永远不要让「裸new」和「裸delete」之间隔着可能抛异常的代码。如果必须用,就用智能指针或RAII包装起来。

两种模式的对比

对比维度 忘记delete 异常安全
根本原因 程序员疏忽 异常破坏执行流
触发条件 所有调用路径 仅异常路径
排查难度 中等(静态分析可发现) 高(需要理解异常流)
最佳对策 智能指针 / 代码审查 RAII / 智能指针

一张图看懂内存泄漏的两种模式

下面这张SVG图,把两种泄漏模式的流程和解决方案画清楚了:

内存泄漏两种核心模式 模式一:忘记delete new分配内存 使用内存(正常路径) ✗ 忘记delete → 内存泄漏 ✅ 对策: • 使用unique_ptr/shared_ptr • 代码审查时检查new/delete配对 • 静态分析工具(如clang-tidy) 模式二:异常安全 new分配内存 中间代码抛异常 ✗ delete被跳过 → 内存泄漏 ✅ 对策: • RAII:资源绑定到栈对象 • 智能指针自动释放 • 避免new/delete之间抛异常

实战建议:如何避免这两种模式

说了这么多,到底怎么落地?我分享几个自己的做法:

  • 写new的同时写delete——哪怕先写个空行占位,也比事后补强
  • 优先用智能指针——std::unique_ptr基本能覆盖90%的场景
  • 异常路径要测试——专门写单元测试触发异常,看资源是否释放
  • 用Valgrind或ASan定期跑——我每周至少跑一次内存检测
💡 小技巧: 如果你非要用裸指针,可以写一个简单的「守卫」类。把delete放在析构函数里,利用栈展开自动释放。这其实就是RAII的雏形。

嗯,这两种模式说完了。其实核心就一句话:别让new和delete之间出现「意外」。要么用智能指针彻底消灭裸new,要么用RAII保证异常安全。你想想看,C++11之后智能指针已经这么成熟了,何必还跟手动delete较劲呢?


专注资料整理