上一篇我们掌握了 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
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 列表、缓存中的非拥有引用。

08_weak_ptr_observer.cpp 用双向链表节点做了正反对比:NodeBad 的 prev 是 shared_ptr,作用域结束后两个节点都不会析构;NodeGood 把 prev 换成 weak_ptr,并在访问时 lock():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NodeGood {
public:
std::string name;
std::shared_ptr<NodeGood> next;
std::weak_ptr<NodeGood> prev; // 使用 weak_ptr 打破循环

void showPrev() const {
if (auto p = prev.lock()) { // 安全访问
std::cout << " " << name << " 的前驱是 " << p->name << "\n";
} else {
std::cout << " " << name << " 没有前驱或前驱已销毁\n";
}
}
};

二、工厂模式:返回 unique_ptr

工厂函数把「创建哪个具体类」集中在一处,返回基类智能指针。05_factory_pattern.cpp 的简单工厂返回的是 unique_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SimpleFactory {
public:
enum class ProductType { A, B };

static std::unique_ptr<Product> create(ProductType type) {
switch (type) {
case ProductType::A:
return std::make_unique<ConcreteProductA>();
case ProductType::B:
return std::make_unique<ConcreteProductB>();
default:
return nullptr;
}
}
};

auto productA = SimpleFactory::create(SimpleFactory::ProductType::A);
productA->use();

好处:

  • 构造逻辑集中,便于校验参数
  • 调用方拿到明确的所有权语义;需要共享时可以直接把 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::shared_ptr<IConnection> getConnection(const std::string& connStr) {
auto it = connections_.find(connStr);

if (it != connections_.end()) {
// 尝试获取现有连接
if (auto conn = it->second.lock()) {
return conn;
}
}

// 创建新连接
auto conn = std::make_shared<RealConnection>(connStr);
connections_[connStr] = conn; // 存储 weak_ptr
return conn;
}

// 成员:std::unordered_map<std::string, std::weak_ptr<IConnection>> connections_;

连接池本身不延长连接寿命:所有客户端都释放后,连接自动关闭;下次请求再创建新连接。

四、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;
void Widget::do_work() { pimpl_->heavy_detail(); }

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Subject {
public:
void attach(std::weak_ptr<Observer> observer) {
observers_.push_back(observer);
}

void notify(const std::string& event) {
auto it = observers_.begin();
while (it != observers_.end()) {
if (auto obs = it->lock()) {
obs->onNotify(event);
++it;
} else {
// 观察者已销毁,移除
it = observers_.erase(it);
}
}
}

private:
std::vector<std::weak_ptr<Observer>> observers_;
};

若用 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
2
3
4
5
class Document {
std::string title_;
std::vector<std::unique_ptr<Section>> sections_;
// 编译器生成的移动/析构即正确;因为 unique_ptr 不可拷贝,拷贝操作会被自动删除
};

若类自己管理裸指针或需要特殊语义,才手写 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
2
3
4
cd ref/cpp_demo/smart_pointers
./build.sh --run # 依次运行全部 9 个程序
./build.sh --run 08_weak_ptr_observer # 只运行某一节
./build.sh --run demo_share_unique # 输出 unique_ptr / shared_ptr 耗时与比值

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

九、小结

概念 要点
循环引用 双向 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 与类型推导 ✅