智能指针与STL容器:存储智能指针的容器
嗯,这个话题很有意思。我见过不少C++开发者,写了好几年代码,还是不敢把智能指针往容器里放。说白了,就是怕搞出内存泄漏或者悬空指针。今天咱们就把这事彻底聊透。
为什么要在容器里存智能指针?
先问个问题:你直接在容器里存对象不行吗?当然行。但有些场景,存指针是更好的选择:
- 多态需求:容器里要存基类指针,指向不同的派生类对象
- 对象共享:同一个对象要被多个容器引用
- 避免拷贝开销:对象很大,拷贝一次心疼半天
- 指针语义:需要null表示"没有对象"
我个人习惯是:能用值就别用指针。但一旦决定用指针,绝对不用裸指针。为什么?你想想看,裸指针在容器里,异常安全、内存管理全得自己手搓,一个不小心就泄漏了。
存储 shared_ptr 的容器
这是最常见的用法。每个元素都是 shared_ptr,容器销毁时自动释放引用计数。
#include <vector>
#include <memory>
#include <iostream>
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " created\n";
}
~Resource() {
std::cout << "Resource " << id_ << " destroyed\n";
}
void work() const { /* ... */ }
private:
int id_;
};
void demo_shared_ptr_vector() {
std::vector<std::shared_ptr<Resource>> resources;
resources.push_back(std::make_shared<Resource>(1));
resources.push_back(std::make_shared<Resource>(2));
// 多个容器共享同一个对象
std::shared_ptr<Resource> shared = resources[0];
std::vector<std::shared_ptr<Resource>> another;
another.push_back(shared);
// 容器销毁时,引用计数减1
// 只有当所有引用都消失,对象才被销毁
}
关键点:容器销毁时,每个 shared_ptr 的析构函数被调用,引用计数减1。只有当引用计数降到0,管理的对象才被释放。
存储 unique_ptr 的容器
unique_ptr 是独占所有权的。把它放进容器,意味着容器唯一拥有这些对象。
#include <vector>
#include <memory>
void demo_unique_ptr_vector() {
std::vector<std::unique_ptr<Resource>> resources;
// 正确方式:用 push_back 传入右值
resources.push_back(std::make_unique<Resource>(1));
resources.push_back(std::make_unique<Resource>(2));
// 或者用 emplace_back 直接构造
resources.emplace_back(std::make_unique<Resource>(3));
// 注意:不能拷贝 unique_ptr
// auto copy = resources[0]; // 编译错误!
// 但可以移动
auto ptr = std::move(resources[0]);
// 现在 resources[0] 是 nullptr
}
我曾经踩过的坑:用 std::vector<std::unique_ptr<T>> 时,千万别用初始化列表。比如 std::vector<std::unique_ptr<T>> vec{ ptr1, ptr2 }; 这种写法,在某些编译器版本上会尝试拷贝 unique_ptr,直接编译失败。老老实实用 push_back 或者 emplace_back。
存储 weak_ptr 的容器
weak_ptr 不增加引用计数。它用来打破循环引用,或者实现"观察者"模式。
#include <vector>
#include <memory>
class Observer {
public:
void update() { /* ... */ }
};
class Subject {
public:
void addObserver(std::shared_ptr<Observer> obs) {
// 存 weak_ptr,不增加引用计数
observers_.push_back(obs);
}
void notify() {
// 遍历时检查 weak_ptr 是否过期
for (auto& weak : observers_) {
if (auto shared = weak.lock()) {
shared->update();
}
// 如果对象已销毁,weak.lock() 返回 nullptr
}
}
private:
std::vector<std::weak_ptr<Observer>> observers_;
};
实用技巧:用 weak_ptr 容器时,记得定期清理过期的条目。不然容器里全是"空壳",遍历时白白浪费 lock() 的开销。我一般会在 notify() 里顺手清理:observers_.erase(std::remove_if(...), observers_.end());
容器选择与性能考量
不同的容器,对智能指针的支持程度不一样。我整理了一张表:
| 容器类型 | shared_ptr | unique_ptr | weak_ptr | 注意事项 |
|---|---|---|---|---|
| vector | ✅ 完美支持 | ✅ 支持(移动语义) | ✅ 支持 | unique_ptr 需要移动,不能拷贝 |
| list | ✅ 完美支持 | ✅ 支持 | ✅ 支持 | 插入删除不导致迭代器失效 |
| map/set | ✅ 完美支持 | ✅ 支持 | ✅ 支持 | 注意比较器的定义 |
| array | ✅ 支持 | ❌ 不支持 | ✅ 支持 | array 大小固定,unique_ptr 无法移动构造 |
| unordered_map | ✅ 完美支持 | ✅ 支持 | ✅ 支持 | 哈希函数需要处理智能指针 |
性能提示:shared_ptr 的拷贝涉及原子操作,频繁拷贝会影响性能。如果你只是遍历容器,用 const reference 获取元素:const auto& ptr = vec[i];,避免不必要的引用计数增减。
常见陷阱与最佳实践
我这些年踩过的坑,总结成几条铁律:
- 别把 shared_ptr 和裸指针混着用。比如从容器里取出 raw pointer 存到别处,那边对象销毁了,这边还傻傻地访问。
- unique_ptr 容器不支持拷贝构造和拷贝赋值。如果你需要复制容器,得手动实现深拷贝。
- weak_ptr 容器要定期清理。不然容器越来越大,全是过期的 weak_ptr。
- 用 make_shared 和 make_unique。别用 new,异常安全更好,而且 make_shared 还能减少一次内存分配。
我曾经犯过的错:在项目里用 std::vector<std::shared_ptr<T>> 存储大量对象,然后频繁地 push_back 和 erase。结果性能惨不忍睹。后来发现,每次 erase 都会导致后续元素的 shared_ptr 拷贝(引用计数原子操作),大量元素时就是灾难。解决方案:改用 std::list,或者用 std::vector 但标记删除,批量清理。
知识体系图
下面这张图总结了智能指针与STL容器的核心关系:
总结
说白了,智能指针和STL容器是天作之合。你只要记住三条:
- unique_ptr 给容器,容器就是唯一主人
- shared_ptr 给容器,大家共享所有权
- weak_ptr 给容器,你只是旁观者
选对了,代码既安全又高效。选错了,嗯...你可能会在调试内存泄漏的路上度过一个愉快的周末。
我的个人建议:新项目里,能用 unique_ptr 就别用 shared_ptr。unique_ptr 语义清晰,性能更好。等真需要共享所有权了,再换成 shared_ptr 也不迟。别一开始就上 shared_ptr,那玩意儿用多了,引用计数绕来绕去,自己都搞不清谁还活着。
公众号:蓝海资料掘金营,微信deep3321