std::weak_ptr 是 std::shared_ptr 的辅助智能指针,不独立存在,特点如下:

  1. 不参与资源所有权共享,不影响指向对象的引用计数
  2. 能检测自身是否悬空(过期),即指向的对象是否已被销毁
  3. 无法直接解引用、无法直接判空,需借助 std::shared_ptr 实现安全访问

基础用法

创建

从 std::shared_ptr 直接创建,两者指向同一对象,且不会增加对象的引用计数:

1
2
3
auto spw = std::make_shared<Widget>(); // 对象引用计数(RC)=1
std::weak_ptr<Widget> wpw(spw); // RC仍为1,wpw与spw指向同一对象
spw = nullptr; // RC=0,对象销毁,wpw悬空(过期)

安全访问对象(解决竞态条件)

单独调用 expired() 判断是否过期存在竞态风险(检查后到访问前,对象可能被其他线程销毁),需通过原子操作同时完成 “检查 + 访问”,有两种方式:

方式 操作 效果
lock() 成员函数 auto sp = wpw.lock(); 若 wpw 未过期,返回指向对象的 shared_ptr;若过期,返回空 shared_ptr
shared_ptr 构造函数 std::shared_ptr<Widget> sp(wpw); 若 wpw 未过期,构造成功;若过期,抛出 std::bad_weak_ptr 异常

应用场景

std::weak_ptr 用于处理 shared_ptr 可能悬空的场景,有三类:

1. 可自动清理的缓存(工厂函数优化)

  • 需求:工厂函数加载对象的操作昂贵,需缓存结果;同时要求对象无人使用时,缓存自动销毁,避免占用资源

  • 实现方案:

    • 缓存容器存储 std::weak_ptr<const Widget>,不影响对象生命周期
    • 工厂函数返回 std::shared_ptr<const Widget>,由调用者管理对象生命周期
    • 每次请求时,通过 lock() 检查缓存对象是否有效,无效则重新加载并缓存
1
2
3
4
5
6
7
8
9
10
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // 原子化检查+获取
if (!objPtr) {
objPtr = loadWidget(id); // 加载新对象
cache[id] = objPtr; // 存入缓存
}
return objPtr;
}

2. 观察者模式的观察者列表

  • 需求:被观察者(subject)需持有观察者(observer)的指针,用于状态变更通知;但被观察者不控制观察者的生命周期,需避免访问已销毁的观察者
  • 实现方案:被观察者的观察者列表存储 std::weak_ptr<Observer>,通知前通过 lock() 检查观察者是否有效,无效则跳过

3. 打破 std::shared_ptr 循环引用

  • 问题:若两个对象互相持有对方的 std::shared_ptr,会形成循环引用,导致引用计数无法归零,对象无法销毁,造成内存泄漏
  • 解决:将其中一方的 shared_ptr 替换为 weak_ptr,weak_ptr 不影响对方的引用计数,打破循环
  • 补充:严格分层结构(如树)中,子节点生命周期由父节点管理,子到父的反向引用可用原始指针,无需 weak_ptr

效率特性

  1. 大小:与 std::shared_ptr 相同
  2. 底层:与 std::shared_ptr 共用控制块
  3. 性能:构造、析构、赋值操作涉及原子操作,效率与 shared_ptr 基本一致
  4. 引用计数细节:控制块存在两个引用计数,weak_ptr 操作的是第二个计数

总结

  1. 用 std::weak_ptr 替代可能悬空的 std::shared_ptr,避免悬空访问的未定义行为
  2. std::weak_ptr 的三大典型场景:缓存、观察者列表、打破 shared_ptr 循环引用