当前位置: 首页 > news >正文

对于类似std::shared_ptr但有可能空悬的指针使用std::weak_ptr: Effective Modern C++ 条款20

你提供的这篇关于 std::weak_ptr 的文章内容非常清晰、结构合理,很好地总结了其核心机制和三大典型应用场景(缓存、观察者模式、打破循环引用)。下面我将基于你的原文进行系统化整理与扩展说明,帮助读者更深入理解 std::weak_ptr 的设计哲学及其在现代 C++ 中的重要性。


✅ 什么是 std::weak_ptr

std::weak_ptr 是一个“非拥有型”智能指针,它指向由 std::shared_ptr 管理的对象,但不会增加该对象的引用计数。

📌 核心特性:

特性描述
不影响引用计数使用 weak_ptr 不会使目标对象的 shared_ptr 引用计数 +1 或 -1
可检测悬空状态调用 .expired() 判断是否已失效(即原对象已被释放)
解引用需原子操作必须通过 .lock() 创建临时 shared_ptr 来安全访问对象
支持线程安全多线程环境下可安全使用(仅检查过期状态)
auto sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp(sp); // wp 不影响 sp 的引用计数(RC=1)
sp.reset(); // RC变为0,Widget被销毁 → wp now expired
if (wp.expired()) { /* do something */ }

⚠️ 为什么不能直接解引用 std::weak_ptr

这是关键点!
如果你这样写:

if (!wp.expired()) {wp.get(); // ❌ 危险!可能已经析构!
}

存在竞态条件风险:

  • 在调用 expired()get() 之间,另一个线程可能把 shared_ptr 设为 nullptr
  • 此时 wp.get() 返回的是一个悬空指针,会导致未定义行为(UB)!

✅ 正确做法:使用 .lock() 原子性地获取 shared_ptr

auto locked = wp.lock();
if (locked) {locked->doSomething(); // 安全访问
} else {// 对象已销毁,无需继续处理
}

💡 .lock() 是原子操作:它同时完成检查和创建 shared_ptr,避免竞态问题。


🔍 三种经典使用场景详解

1️⃣ 缓存(Cache)

场景痛点:

  • 工厂函数返回昂贵资源(如图像加载、数据库查询)。
  • 若缓存中存储 unique_ptrshared_ptr,会导致对象永远无法释放(即使没人用了)。
  • 需要一种方式知道缓存条目是否还有效(即原始对象是否已被销毁)。

解决方案:

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) {static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;auto it = cache.find(id);if (it != cache.end()) {auto objPtr = it->second.lock(); // 检查是否仍有效if (objPtr) return objPtr;      // 存在且有效,直接返回}// 缓存未命中或失效,重新加载并更新缓存auto newPtr = loadWidget(id);cache[id] = newPtr; // 保存到缓存(weak_ptr)return newPtr;
}

✅ 优势:

  • 缓存不阻止对象销毁;
  • 当客户端不再持有对象时,缓存自动失效;
  • 内存效率高,适合长期运行的应用程序(如游戏引擎、Web服务器)。

2️⃣ 观察者模式(Observer Pattern)

经典问题:

  • Subject 持有 Observer 的强引用(shared_ptr),导致 Observer 无法自行销毁;
  • 如果 Observer 被销毁后,Subject 还试图通知它 → 悬空指针 → UB!

解决方案:

class Subject {
private:std::vector<std::weak_ptr<Observer>> observers;
public:void addObserver(std::shared_ptr<Observer> obs) {observers.push_back(obs);}void notify() {for (auto& weak : observers) {if (auto shared = weak.lock()) { // 安全访问shared->update();} else {// 观察者已销毁,移除无效项(可选优化)observers.erase(std::remove_if(observers.begin(), observers.end(),[](const std::weak_ptr<Observer>& w) { return !w.lock(); }),observers.end());}}}
};

✅ 优势:

  • Observer 自主管理生命周期;
  • Subject 不阻塞 Observer 销毁;
  • 自动清理无效观察者,防止内存泄漏。

3️⃣ 打破循环引用(Breaking Circular References)

典型场景:
class A {
public:std::shared_ptr<B> b;
};class B {
public:std::shared_ptr<A> a;
};

如果 AB 相互持有对方的 shared_ptr,则它们永远不会被释放(引用计数永远 ≥1)——这就是著名的 循环引用导致内存泄漏

解决方案:

将其中一个方向改为 weak_ptr

class A {
public:std::shared_ptr<B> b;
};class B {
public:std::weak_ptr<A> a; // 关键改动!不再是 shared_ptr
};

✅ 效果:

  • AB 可以正常析构;
  • B 可以安全检查 a.lock() 是否有效后再使用;
  • 既保留了双向关系,又避免了内存泄漏。

📌 小贴士:
这种模式常见于 GUI 控件(如父控件持有子控件,子控件也记录父控件)或依赖图谱中。


🧠 总结:何时该用 std::weak_ptr

使用场景推荐理由
缓存/映射表避免永久持有对象,支持自动清理
观察者列表不干扰观察者的生命周期,防止悬空访问
打破循环引用清除无意义的共享所有权,释放资源
临时引用需求如调试打印、日志记录等,不想影响对象寿命

✅ 一句话口诀记忆:
“想用指针但不想‘占着茅坑不拉屎’,就用 weak_ptr!”


🛠️ 最佳实践建议

建议说明
✅ 总是先调用 .lock() 再访问对象避免竞态条件和悬空指针
✅ 使用 auto sp = wp.lock() 更简洁类型推导+安全性兼顾
✅ 不要用 wp.get() 直接解引用危险!容易出错
✅ 合理使用 expired() 检查用于快速过滤无效条目(比如在容器遍历前)
✅ 注意性能开销lock() 是轻量级原子操作,不影响性能

如果你正在学习《Effective Modern C++》这本书,这篇文章正是 Item 20 的精炼版解读。
建议结合书中具体代码示例一起阅读,会更加深刻理解为什么 std::weak_ptr 是现代 C++ 内存管理不可或缺的一环。
需要进一步分析某个场景的实现细节?欢迎继续提问!

http://www.dtcms.com/a/314363.html

相关文章:

  • Shell 脚本发送信号给 C 应用程序,让 C 应用程序回收线程资源后自行退出。
  • Linux服务器管理MySQL数据库的常见命
  • Spring AI 系列之三十三 - Spring AI Alibaba-Graph框架之人类反馈
  • 区块链基础之Merkle B+树
  • 【Spring】SpringBoot自动注入原理分析,@SpringBootApplication、@EnableAutoConfiguration详解
  • Java类与对象练习题
  • 运动想象 (MI) 分类学习系列 (18) : MSVTNet
  • 一(1)关于单链表中的疑问
  • Spring AI实战:SpringBoot项目结合Spring AI开发——提示词(Prompt)技术与工程实战详解
  • SAP-ABAP:ABAP Open SQL 深度解析:核心特性、性能优化与实践指南
  • 设计模式 -> 策略模式(Strategy Pattern)
  • 2025年8月4日私鱼创作平台v1.0.4公测版更新发布-完成大部分功能包含关注创作者以及发布作品及合集功能优雅草科技
  • 06 基于sklearn的机械学习-欠拟合、过拟合、正则化、逻辑回归
  • 线程互斥锁:守护临界区的关键
  • 可编辑190页PPT | 某科技集团数字化转型SAP解决方案
  • Vue 3 版本的 JWT 单点登录 (SSO) 实现
  • 国家科学技术奖答辩PPT案例_科技进步奖ppt制作_技术发明奖ppt设计美化_自然科学奖ppt模板 | WordinPPT
  • 使用mybatis生成器生成实体类mapper和查询参数文件,实现简单增删改查。使用log4j输出日志到控制台。使用配置文件注册Bean,配置视图解析器
  • 【Java】使用模板方法模式设计EasyExcel批量导入导出
  • Apache Camel 中 ProducerTemplate
  • 刷题日志(7)——二叉树高频习题(下)
  • 高精度实战:YOLOv11交叉口目标行为全透视——轨迹追踪×热力图×滞留分析(附完整代码)
  • FrePrompter: Frequency self-prompt for all-in-one image restoration
  • Opencv[一]
  • R 语言科研绘图第 67 期 --- 箱线图-显著性
  • Spark SQL:用SQL玩转大数据
  • OpenCV轻松入门_面向python(第二章图像处理基础)
  • 论文阅读笔记:《Dataset Distillation by Matching Training Trajectories》
  • 【数据结构初阶】--算法复杂度详解
  • 登录弹窗,cv直接使用