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()缩小容量时,同样不会释放指针指向的对象。容器只负责指针本身,不负责指针指向的东西。
为什么容易忽略?
我个人觉得,原因有两个:
- 心理惯性——我们习惯了
std::string、std::vector<int>这些值类型容器,觉得容器销毁了,里面的东西自然就没了。但指针不是值,它只是一个地址。 - 代码分散——
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机制。
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:多线程环境下,一个线程在遍历容器,另一个线程
erase并delete了对象。遍历线程拿到的是悬空指针。解决方案:用std::shared_ptr配合读写锁,或者用copy-on-write策略。
知识结构图
下面这张图总结了容器中指针未释放的核心问题和解决方案:
总结
容器中指针未释放,说白了就是「容器只管存地址,不管地址指向的东西」。解决方案就三条路:
- 手动释放——适合小规模、逻辑简单的场景,但容易漏
- 智能指针——最推荐,RAII机制自动管理生命周期
- 对象池——适合需要复用对象的场景,但要保证析构时释放干净
我个人现在写代码,只要容器里存的是堆上对象,一律用std::unique_ptr或std::shared_ptr。省心,也省得review时跟同事扯皮。你想想看,与其花时间排查内存泄漏,不如一开始就选对工具,对吧?
公众号:蓝海资料掘金营,微信deep3321