上一篇我们掌握了 unique_ptrshared_ptr 的基本用法。但在真实项目里,智能指针不只是替代 new/delete——循环引用、工厂函数、Pimpl、Observer 等场景会逼你思考「谁该拥有、谁该观察」。

这一篇是智能指针的下半场weak_ptr 如何打破循环引用,以及几种经典设计模式如何用智能指针落地。

这是「现代 C++ 实战」系列的第 5 篇。对应 demo:ref/cpp_demo/smart_pointers/ 第 05–09 节。建议先读 第 04 篇:智能指针(上)

一、weak_ptr 与循环引用

shared_ptr 的引用计数是双向的:A 持有 B,B 也持有 A 时,计数永远不归零——内存泄漏

1
2
3
4
5
struct Node {
std::shared_ptr<Node> next;
// std::shared_ptr<Node> parent; // 危险:与子节点形成环
std::weak_ptr<Node> parent; // 安全:不增加父节点计数
};
指针类型 是否拥有对象 是否增加强引用计数
shared_ptr
weak_ptr 否(观察) 否(只增加弱引用计数)

使用 weak_ptr 前必须检查对象是否还活着:

1
2
3
4
5
if (auto p = parent.lock()) {
p->do_something(); // 临时 shared_ptr,作用域结束计数 -1
} else {
// 对象已被销毁
}

典型场景:树/图的父指针、Observer 列表、缓存中的非拥有引用。

二、工厂模式 + make_shared

工厂函数常返回 shared_ptr,配合私有构造函数控制创建入口:

1
2
3
4
5
6
7
8
9
10
11
12
class Widget {
public:
static std::shared_ptr<Widget> create(int id) {
return std::make_shared<Widget>(id);
}
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;

private:
explicit Widget(int id) : id_(id) {}
int id_;
};

好处:

  • 构造逻辑集中,便于校验参数
  • 调用方拿到明确的所有权语义
  • make_shared 一次分配对象 + 控制块

三、代理模式:控制远程对象生命周期

代理持有真实对象的 shared_ptr,对外提供受限接口——真实对象在所有代理销毁后才释放:

1
2
3
4
5
6
class ServiceProxy {
std::shared_ptr<Service> impl_;
public:
explicit ServiceProxy(std::shared_ptr<Service> s) : impl_(std::move(s)) {}
void invoke() { if (impl_) impl_->run(); }
};

多个 ServiceProxy 共享同一 Service,适合连接池、资源句柄包装等。

四、Pimpl:编译防火墙

Pointer to Implementation 把实现细节藏进 .cpp,头文件只暴露接口——减少编译依赖、加快增量构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// widget.h
class Widget {
struct Impl;
std::unique_ptr<Impl> pimpl_;
public:
Widget();
~Widget();
void do_work();
};

// widget.cpp
struct Widget::Impl {
void heavy_detail() { /* ... */ }
};
Widget::Widget() : pimpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::do_work() { pimpl_->heavy_detail(); }

unique_ptr 适合 Pimpl:实现类独占、不需共享;析构函数必须在 .cpp 中定义(Impl 完整类型可见)。

五、Observer:weak_ptr 作为观察者句柄

Subject 维护一组观察者,但不应拥有观察者生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Subject {
std::vector<std::weak_ptr<Observer>> observers_;
public:
void attach(std::shared_ptr<Observer> o) {
observers_.push_back(o);
}
void notify() {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto o = it->lock()) {
o->on_event();
++it;
} else {
it = observers_.erase(it); // 观察者已销毁,清理
}
}
}
};

若用 shared_ptr<Observer> 存储,Subject 与 Observer 可能互相 shared_ptr 导致泄漏;weak_ptr 只观察、不延长寿命。

六、Rule of Zero / Rule of Five

类成员全是智能指针(或标准容器)时,通常 Rule of Zero——五个特殊成员函数全部 = default 即可:

1
2
3
4
5
class Document {
std::string title_;
std::vector<std::unique_ptr<Section>> sections_;
// 编译器生成的拷贝/移动/析构通常正确
};

若类自己管理裸指针或需要特殊语义,才手写 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/ 后半部分通常包含:

主题
05 weak_ptr 与循环引用演示
06 工厂 + 私有构造
07 代理模式
08 Pimpl 编译防火墙
09 Observer + weak_ptr
1
2
cd ref/cpp_demo/smart_pointers
./build.sh --run

建议对照源码里的构造/析构日志,观察引用计数变化与对象销毁时机。

九、小结

概念 要点
循环引用 双向 shared_ptr 泄漏 → 一侧改 weak_ptr
工厂 make_shared + 私有构造,统一创建入口
Pimpl unique_ptr<Impl> 隐藏实现,析构放 .cpp
Observer weak_ptr 观察,.lock() 使用前检查
选型 unique 默认,shared 真共享,weak 只观察

现代 C++ 实战系列第 5 篇完。下一篇进入 Lambda 与类型推导——现代 C++ 的瑞士军刀。

系列导航

篇号 标题 状态
04 智能指针(上)
05 智能指针(下):模式与循环引用(本篇)
06 Lambda 与类型推导 下一篇

完整大纲见工作区 docs/CPP_SERIES_OUTLINE.md