27. 内存泄漏常见模式:容器中指针未释放

说实话,容器里存指针导致内存泄漏,是我见过最频繁的bug之一。我自己刚入行那会儿也踩过这个坑——写了个缓存系统,用std::vector<MyClass*>存了一堆对象,程序跑了一整天,内存蹭蹭往上涨。当时我还纳闷:明明调了clear()啊,怎么内存没回来?

嗯,问题就出在:clear()只清空了容器里的指针值,并没有释放指针指向的对象。说白了,容器只负责管理它自己的内存,不负责管理你塞进去的那些对象的生命周期。

典型场景:裸指针容器

先看一个最常见的错误写法:

std::vector<MyClass*> vec;
for (int i = 0; i < 1000; ++i) {
    vec.push_back(new MyClass(i));
}
// 使用 vec ...
vec.clear();  // ❌ 内存泄漏!对象没有被 delete

你想想看,clear()只是把vector内部的指针数组清空了,那些new出来的MyClass对象还孤零零地待在堆上,没人管了。这就是典型的内存泄漏。

⚠️ 注意:不只是clear()erase()pop_back()resize()缩小容量时,同样不会释放指针指向的对象。容器只负责指针本身,不负责指针指向的东西。

为什么容易忽略?

我个人觉得,原因有两个:

  1. 心理惯性——我们习惯了std::stringstd::vector<int>这些值类型容器,觉得容器销毁了,里面的东西自然就没了。但指针不是值,它只是一个地址。
  2. 代码分散——new在一个地方,delete在另一个地方,中间隔了十几行甚至跨函数,很容易漏掉。

我在项目中遇到过这样一个案例:一个同事用std::map<int, Device*>管理设备对象,设备上线时new,下线时从map里erase。结果程序跑了一周,内存涨了2GB。查了半天才发现,有一个异常分支里只调了erase,忘了delete。嗯,这种问题在代码review时很难一眼看出来。

正确做法:手动释放

如果你非要用裸指针容器,那就要手动释放:

// 方式1:遍历释放
for (auto* p : vec) {
    delete p;
}
vec.clear();

// 方式2:使用算法
std::for_each(vec.begin(), vec.end(), [](MyClass* p) { delete p; });
vec.clear();

但说实话,这种方式太容易出错了。你想想看,如果容器里混了nullptr怎么办?如果同一个指针被多个容器持有怎么办?如果异常发生时还没来得及释放怎么办?

更好的方案:智能指针容器

我个人强烈建议——用智能指针替代裸指针。这是最省心的方案:

// 独占所有权:std::unique_ptr
std::vector<std::unique_ptr<MyClass>> vec;
vec.push_back(std::make_unique<MyClass>(i));
// 容器销毁时,所有对象自动释放 ✅

// 共享所有权:std::shared_ptr
std::vector<std::shared_ptr<MyClass>> vec;
vec.push_back(std::make_shared<MyClass>(i));
// 最后一个引用消失时自动释放 ✅

用了智能指针之后,clear()erase()、甚至容器析构时,都会自动调用智能指针的析构函数,从而释放堆上的对象。说白了,就是把「手动管理内存」这件事交给了RAII机制。

💡 小技巧:如果你需要把同一个对象放入多个容器(比如一个按ID索引,一个按名称索引),用std::shared_ptr。如果对象只属于一个容器,用std::unique_ptr。不要混用裸指针和智能指针,否则智能指针释放了对象,裸指针就成了悬空指针。

特殊场景:对象池与资源复用

有一种情况比较特殊——对象池。比如你预先创建一批对象,反复使用,不销毁。这时候用裸指针反而更合适:

class ObjectPool {
    std::vector<MyClass*> pool_;
public:
    ~ObjectPool() {
        for (auto* p : pool_) delete p;  // 析构时统一释放
    }
    // ...
};

但要注意:对象池的析构函数必须释放所有对象。我曾经见过一个对象池,只释放了部分对象——因为有一个分支提前return了,后面的delete没执行到。嗯,这种问题用智能指针就能避免。

避坑指南

我曾经踩过几个坑,分享给你:

  • 坑1:容器里同时存了new出来的对象和栈上对象的地址。释放时delete栈上地址,直接崩溃。解决方案:统一用智能指针,或者统一从堆上分配。
  • 坑2:std::vector<std::unique_ptr<T>>时,不小心调了vec[0].get()拿到裸指针,然后手动delete了它。结果unique_ptr析构时二次释放,又崩了。记住:要么全自动,要么全手动,别混着来。
  • 坑3:多线程环境下,一个线程在遍历容器,另一个线程erasedelete了对象。遍历线程拿到的是悬空指针。解决方案:用std::shared_ptr配合读写锁,或者用copy-on-write策略。

知识结构图

下面这张图总结了容器中指针未释放的核心问题和解决方案:

容器中指针未释放 — 知识结构 容器中裸指针未释放 内存泄漏 程序运行缓慢/崩溃 资源耗尽 解决方案 方案一:手动释放 遍历 delete + clear() 方案二:智能指针 unique_ptr / shared_ptr 方案三:对象池 析构时统一释放

总结

容器中指针未释放,说白了就是「容器只管存地址,不管地址指向的东西」。解决方案就三条路:

  • 手动释放——适合小规模、逻辑简单的场景,但容易漏
  • 智能指针——最推荐,RAII机制自动管理生命周期
  • 对象池——适合需要复用对象的场景,但要保证析构时释放干净

我个人现在写代码,只要容器里存的是堆上对象,一律用std::unique_ptrstd::shared_ptr。省心,也省得review时跟同事扯皮。你想想看,与其花时间排查内存泄漏,不如一开始就选对工具,对吧?


公众号:蓝海资料掘金营,微信deep3321