现代 C++ 实战(05):智能指针(下)——模式与循环引用
上一篇我们掌握了 unique_ptr 和 shared_ptr 的基本用法。但在真实项目里,智能指针不只是替代 new/delete——循环引用、工厂函数、Pimpl、Observer 等场景会逼你思考「谁该拥有、谁该观察」。
这一篇是智能指针的下半场:weak_ptr 如何打破循环引用,以及几种经典设计模式如何用智能指针落地。
这是「现代 C++ 实战」系列的第 5 篇。对应 demo:
ref/cpp_demo/smart_pointers/第 05–08 节(外加性能对比demo_share_unique)。建议先读 第 04 篇:智能指针(上)。
一、weak_ptr 与循环引用
shared_ptr 的引用计数是双向的:A 持有 B,B 也持有 A 时,计数永远不归零——内存泄漏。
1 | struct Node { |
| 指针类型 | 是否拥有对象 | 是否增加强引用计数 |
|---|---|---|
shared_ptr |
是 | 是 |
weak_ptr |
否(观察) | 否(只增加弱引用计数) |
使用 weak_ptr 前必须检查对象是否还活着:
1 | if (auto p = parent.lock()) { |
典型场景:树/图的父指针、Observer 列表、缓存中的非拥有引用。
08_weak_ptr_observer.cpp 用双向链表节点做了正反对比:NodeBad 的 prev 是 shared_ptr,作用域结束后两个节点都不会析构;NodeGood 把 prev 换成 weak_ptr,并在访问时 lock():
1 | class NodeGood { |
二、工厂模式:返回 unique_ptr
工厂函数把「创建哪个具体类」集中在一处,返回基类智能指针。05_factory_pattern.cpp 的简单工厂返回的是 unique_ptr:
1 | class SimpleFactory { |
好处:
- 构造逻辑集中,便于校验参数
- 调用方拿到明确的所有权语义;需要共享时可以直接把
unique_ptr转成shared_ptr - 通过基类指针实现多态,所以
Product必须有虚析构函数(demo 第 4 节专门对比了有无virtual ~Base()的析构差异)
demo 里还有工厂方法、抽象工厂(WindowsFactory / MacFactory)和注册式工厂(AnimalFactory)。
如果想把构造函数设为
private、只允许通过静态create()创建,注意std::make_shared/std::make_unique无法访问私有构造函数,需要改用std::shared_ptr<T>(new T(...)),或借助私有的 passkey 标签类型。
三、代理模式:控制远程对象生命周期
06_proxy_pattern.cpp 演示了虚代理(ImageProxy 用 unique_ptr 延迟加载)、保护代理、智能引用代理、缓存代理和日志代理。其中最能体现智能指针价值的是智能引用代理——连接池只用 weak_ptr 记住连接,客户端拿到 shared_ptr 共享同一连接:
1 | std::shared_ptr<IConnection> getConnection(const std::string& connStr) { |
连接池本身不延长连接寿命:所有客户端都释放后,连接自动关闭;下次请求再创建新连接。
四、Pimpl:编译防火墙
Pointer to Implementation 把实现细节藏进 .cpp,头文件只暴露接口——减少编译依赖、加快增量构建。
1 | // widget.h |
unique_ptr 适合 Pimpl:实现类独占、不需共享;析构函数必须在 .cpp 中定义(Impl 完整类型可见)。07_pimpl_pattern.cpp 在此基础上还手写了拷贝构造 / 拷贝赋值(深拷贝 Impl)、= default 的移动操作,并演示了用 shared_ptr 共享状态的 SharedWidget 和带多态的 Shape::Impl。
五、Observer:weak_ptr 作为观察者句柄
Subject 维护一组观察者,但不应拥有观察者生命周期:
08_weak_ptr_observer.cpp 中的 Subject(节选):
1 | class Subject { |
若用 shared_ptr<Observer> 存储,Subject 与 Observer 可能互相 shared_ptr 导致泄漏;weak_ptr 只观察、不延长寿命。同一个文件里还演示了 weak_ptr 做缓存(ObjectCache)、lock() 与 expired() 的区别,以及 enable_shared_from_this(AsyncTask)。
六、Rule of Zero / Rule of Five
类成员全是智能指针(或标准容器)时,通常 Rule of Zero——五个特殊成员函数一个都不用写:
1 | class Document { |
若类自己管理裸指针或需要特殊语义,才手写 Rule of Five(析构、拷贝构造/赋值、移动构造/赋值)。
智能指针让大多数业务类直接 Rule of Zero,减少资源管理 bug。
七、选型指南
| 场景 | 推荐 |
|---|---|
| 单一所有者、工厂返回、Pimpl | unique_ptr |
| 多处共享、容器共享元素 | shared_ptr |
| 观察、打破循环引用、缓存键 | weak_ptr |
| 不拥有、生命周期由调用方保证 | T* / T& |
| 性能关键、明确不转移所有权 | T& 或 span |
决策顺序:能否 unique?→ 必须共享吗?→ 只是观察吗?→ 最后才考虑裸指针。
八、demo 导览:smart_pointers 05–09
ref/cpp_demo/smart_pointers/ 后半部分包含:
| 节 | 源文件 / target | 主题 |
|---|---|---|
| 05 | 05_factory_pattern |
简单工厂、工厂方法、抽象工厂、虚析构函数、注册式工厂 |
| 06 | 06_proxy_pattern |
虚代理、保护代理、智能引用代理(连接池)、缓存代理、日志代理 |
| 07 | 07_pimpl_pattern |
基本 Pimpl、共享状态 Pimpl、编译防火墙、多态 Pimpl |
| 08 | 08_weak_ptr_observer |
循环引用与 weak_ptr、观察者模式、缓存、lock()、enable_shared_from_this |
| — | demo_share_unique |
make_unique 与 make_shared 各创建 100 万个 64 字节对象的耗时对比 |
1 | cd ref/cpp_demo/smart_pointers |
建议对照源码里的构造/析构日志,观察引用计数变化与对象销毁时机。
九、小结
| 概念 | 要点 |
|---|---|
| 循环引用 | 双向 shared_ptr 泄漏 → 一侧改 weak_ptr |
| 工厂 | 返回 unique_ptr<Base>,统一创建入口,基类必须虚析构 |
| Pimpl | unique_ptr<Impl> 隐藏实现,析构放 .cpp |
| Observer | weak_ptr 观察,.lock() 使用前检查 |
| 选型 | unique 默认,shared 真共享,weak 只观察 |
现代 C++ 实战系列第 5 篇完。下一篇进入 Lambda 与类型推导——现代 C++ 的瑞士军刀。
系列导航
| 篇号 | 标题 | 状态 |
|---|---|---|
| 04 | 智能指针(上) | ✅ |
| 05 | 智能指针(下):模式与循环引用(本篇) | ✅ |
| 06 | Lambda 与类型推导 | ✅ |











