智能指针与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];,避免不必要的引用计数增减。

常见陷阱与最佳实践

我这些年踩过的坑,总结成几条铁律:

  1. 别把 shared_ptr 和裸指针混着用。比如从容器里取出 raw pointer 存到别处,那边对象销毁了,这边还傻傻地访问。
  2. unique_ptr 容器不支持拷贝构造和拷贝赋值。如果你需要复制容器,得手动实现深拷贝。
  3. weak_ptr 容器要定期清理。不然容器越来越大,全是过期的 weak_ptr。
  4. 用 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容器核心关系 STL容器 智能指针 管理对象 vector / list / deque map / set / unordered_map array / forward_list shared_ptr 引用计数,共享所有权 unique_ptr 独占所有权,移动语义 weak_ptr 弱引用,打破循环 ⚠️ 注意事项 • unique_ptr 容器不支持拷贝构造,需要移动语义 • shared_ptr 频繁拷贝有原子操作开销 • weak_ptr 容器需定期清理过期条目 • 优先用 make_shared / make_unique • 避免裸指针与智能指针混用 • 考虑用 std::list 减少移动开销

总结

说白了,智能指针和STL容器是天作之合。你只要记住三条:

  • unique_ptr 给容器,容器就是唯一主人
  • shared_ptr 给容器,大家共享所有权
  • weak_ptr 给容器,你只是旁观者

选对了,代码既安全又高效。选错了,嗯...你可能会在调试内存泄漏的路上度过一个愉快的周末。

我的个人建议:新项目里,能用 unique_ptr 就别用 shared_ptr。unique_ptr 语义清晰,性能更好。等真需要共享所有权了,再换成 shared_ptr 也不迟。别一开始就上 shared_ptr,那玩意儿用多了,引用计数绕来绕去,自己都搞不清谁还活着。


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