9. weak_ptr:观察者模式,解决悬空指针
好,咱们今天聊聊 weak_ptr。
说实话,很多 C++ 开发者对 shared_ptr 用得挺溜,但一提到 weak_ptr 就有点懵。我刚开始学的时候也一样——「这玩意儿到底干嘛用的?不就是个弱引用吗?」
嗯,它确实是个弱引用。但它的价值,远比你想象的大。
9.1 为什么需要 weak_ptr?
先问你一个问题:shared_ptr 最大的痛点是什么?
你可能会说「循环引用」。没错,这是经典问题。但我想换个角度说——有时候你只是想「看一眼」某个对象,并不想延长它的生命周期。
举个例子。你有一个缓存系统,缓存里存着一些大对象。你希望当外部没人用这些对象时,缓存能自动释放它们。但缓存本身又需要知道这些对象是否还活着。
如果用 shared_ptr,缓存会一直持有引用计数,对象永远释放不了。这就是典型的「观察者模式」场景——你只需要观察,不需要拥有。
9.2 weak_ptr 的基本用法
先看个最简单的例子:
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加引用计数
std::cout << "use_count: " << sp.use_count() << std::endl; // 输出 1
// 使用前必须 lock()
if (auto locked = wp.lock()) {
std::cout << "value: " << *locked << std::endl;
} else {
std::cout << "对象已被释放" << std::endl;
}
sp.reset(); // 释放 shared_ptr
if (auto locked = wp.lock()) {
std::cout << "value: " << *locked << std::endl;
} else {
std::cout << "对象已被释放" << std::endl; // 走这里
}
return 0;
}
注意看,weak_ptr 不能直接解引用。你必须先调用 lock(),它会返回一个 shared_ptr。如果对象还活着,你就拿到了有效的指针;如果对象已经死了,返回空指针。
我个人习惯把 lock() 叫做「临时借阅证」——你借到了就能用,借不到说明书已经下架了。
9.3 解决循环引用
这是 weak_ptr 最经典的应用场景。我直接上代码:
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 用 weak_ptr 打破循环
int value;
~Node() {
std::cout << "Node " << value << " 被销毁" << std::endl;
}
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->value = 1;
n2->value = 2;
n1->next = n2;
n2->prev = n1; // 这里不会增加引用计数
// 函数结束,n1 和 n2 都能正常释放
return 0;
}
我曾经在一个项目里见过一个双向链表,全部用 shared_ptr 互相指。结果呢?内存泄漏得一塌糊涂。排查了两天才发现是循环引用的问题。从那以后,我定了个规矩:父子关系中,父指向子用 shared_ptr,子指向父用 weak_ptr。
9.4 weak_ptr 的常用操作
| 操作 | 说明 | 示例 |
|---|---|---|
lock() |
返回一个 shared_ptr,如果对象已释放则返回空 | auto sp = wp.lock(); |
expired() |
检查对象是否已被释放 | if (wp.expired()) { ... } |
use_count() |
返回 shared_ptr 的引用计数(不包含 weak_ptr) | int cnt = wp.use_count(); |
reset() |
清空 weak_ptr,不再指向任何对象 | wp.reset(); |
这里有个小细节:expired() 和 lock() 不是原子操作。在多线程环境下,你不能先判断 expired() 再 lock(),因为中间可能被其他线程释放。正确的做法是直接 lock(),然后检查返回值。
9.5 观察者模式实战
咱们来写一个完整的观察者模式例子。假设你有一个「主题」和多个「观察者」。主题状态变化时通知所有观察者。但观察者可能随时被销毁。
#include <memory>
#include <vector>
#include <iostream>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(int state) = 0;
};
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void addObserver(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify(int state) {
for (auto it = observers.begin(); it != observers.end(); ) {
if (auto obs = it->lock()) {
obs->update(state);
++it;
} else {
// 观察者已销毁,清理掉
it = observers.erase(it);
}
}
}
};
class ConcreteObserver : public Observer {
int id;
public:
ConcreteObserver(int i) : id(i) {}
void update(int state) override {
std::cout << "观察者 " << id << " 收到状态: " << state << std::endl;
}
};
int main() {
Subject subject;
auto obs1 = std::make_shared<ConcreteObserver>(1);
auto obs2 = std::make_shared<ConcreteObserver>(2);
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.notify(100); // 两个观察者都收到
obs1.reset(); // 销毁观察者1
subject.notify(200); // 只有观察者2收到,观察者1被自动清理
return 0;
}
你看,用 weak_ptr 实现观察者模式,代码干净又安全。观察者死了,主题不需要手动解注册,下次通知时自动清理。这就是我常说的「弱引用让生命周期管理变得优雅」。
9.6 weak_ptr 的内部机制
你可能会好奇:weak_ptr 怎么知道对象还活着?
其实 shared_ptr 和 weak_ptr 共享一个「控制块」。控制块里有两个计数:
- 引用计数(use_count):记录 shared_ptr 的数量
- 弱计数(weak_count):记录 weak_ptr 的数量
当引用计数降到 0 时,对象被销毁。但控制块还活着,直到弱计数也降到 0。
所以 weak_ptr 能通过控制块知道对象是否还活着。这就是为什么 lock() 能返回一个有效的 shared_ptr——如果对象还在,它临时增加引用计数,让你安全使用。
9.7 知识体系图
下面这张图帮你理清 weak_ptr 的核心逻辑:
9.8 避坑指南
最后,分享几个我踩过的坑:
- 不要用 weak_ptr 存储需要长期持有的数据——它随时可能失效。如果你需要保证对象活着,直接用 shared_ptr。
- lock() 返回的 shared_ptr 用完后记得释放——虽然它不会造成泄漏,但长时间持有会阻止对象被释放。
- 不要在 weak_ptr 上调用 operator* 或 operator->——它不支持直接解引用。我见过有人硬写
*wp,编译都过不了。 - 多线程环境下,lock() 是安全的——它内部有原子操作。但如果你先 expired() 再 lock(),那就不是原子的了。
我曾经在一个高并发服务器里,用 weak_ptr 实现了连接池的自动清理。每个连接被 shared_ptr 持有,连接池用 weak_ptr 观察。连接断开后,shared_ptr 释放,weak_ptr 自动失效,池子下次检查时自动清理。整个设计非常优雅,几乎没有内存泄漏的问题。
嗯,这就是 weak_ptr 的精髓。它不抢 ownership,不干涉生命周期,只是在旁边默默观察。需要的时候借给你用一下,不需要了也不纠缠。
说白了,weak_ptr 就是 C++ 智能指针家族里的「观察者」。它让代码更安全,让设计更清晰。用好它,你的 C++ 代码会上一个台阶。
公众号:蓝海资料掘金营,微信deep3321