条款20:当stdshared_ptr可能悬空时使用stdweak_ptr
std::weak_ptr 观察 shared_ptr 管理的对象,避免循环引用导致资源永不释放。
条款20:当 std::shared_ptr 可能悬空时使用 std::weak_ptr
核心思想
std::weak_ptr
是一个非拥有型(non-owning)智能指针, 它指向std::shared_ptr
所管理的对象,但不增加引用计数。std::weak_ptr
可以在对象被销毁时检测出自己是否悬空(expired)。
设计动机
- 有时候想引用一个对象,但不想拥有它的生命周期。
std::shared_ptr
循环引用(互相持有)会导致内存泄露。- 原始指针无法检测是否悬空,存在未定义行为的风险。
基本用法
1
2
3
4
5
6
7
8
9
auto sp = std::make_shared<Widget>(); // 创建 shared_ptr,引用计数(strong count)为1
std::weak_ptr<Widget> wp(sp); // 创建 weak_ptr,引用计数不变(仍为1),weak count +1
if (!wp.expired()) { // 判断 weak_ptr 是否指向已销毁对象(非原子操作,存在竞态风险)
auto sp2 = wp.lock(); // 原子操作:尝试获取 shared_ptr,若成功,strong count +1
if (sp2) { // 如果 lock 成功,即对象仍存在
// 使用 sp2 访问 Widget,确保对象在此作用域内存活
}
}
wp.lock()
:尝试获取shared_ptr
,失败则返回空。std::shared_ptr<T> sp(wp);
:另一种构造方式,wp过期则抛出std::bad_weak_ptr
异常。
适用场景
缓存系统
假设有一个比较昂贵的加载函数 loadWidget
,它返回一个 std::shared_ptr<const Widget>
:
1
2
3
4
5
// 模拟昂贵的加载操作
std::shared_ptr<const Widget> loadWidget(WidgetID id) {
// 比如从文件、数据库加载数据,构造 Widget 对象
return std::make_shared<const Widget>(/* ... */);
}
我们想写一个“快速加载”的函数 fastLoadWidget
,它带缓存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) {
// 缓存:key 是 WidgetID,value 是 weak_ptr,不增加对象引用计数
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
// 尝试从缓存获取 shared_ptr(会尝试提升 weak_ptr)
auto cachedPtr = cache[id].lock();
if (!cachedPtr) { // 如果缓存不存在或已经过期(对象被销毁)
// 调用真正的加载函数
cachedPtr = loadWidget(id);
// 缓存最新的 weak_ptr,不增加引用计数
cache[id] = cachedPtr;
}
// 返回 shared_ptr,调用者获得对象所有权(引用计数+1)
return cachedPtr;
}
说明:
- 缓存存储
std::weak_ptr
:std::shared_ptr
拥有对象的所有权,会增加引用计数,延长对象生命周期。 如果缓存存的是shared_ptr
,即使程序里其他地方都不再用这个对象,缓存里还持有一个shared_ptr
, 对象的引用计数不会归零,导致对象一直不会被销毁,造成内存泄漏。而
std::weak_ptr
不拥有对象所有权,不增加引用计数,缓存存它只“观察”对象是否存在。当所有真正持有对象的shared_ptr
都销毁后,对象被释放,缓存中的weak_ptr
就变成“过期”状态(expired)。
std::weak_ptr
有接受对应std::shared_ptr
的构造函数和赋值操作符std::weak_ptr<T>
是专门为与std::shared_ptr<T>
配合设计的,- 它有一个 构造函数 和 赋值运算符,可以接受
std::shared_ptr<T>
类型, - 并从中构造对应的
weak_ptr
(指向相同对象,但不增加引用计数)。
调用
lock()
尝试获取shared_ptr
: 若对象存在,返回有效的shared_ptr
,引用计数+1。若对象已销毁,返回空指针。若缓存失效,重新加载并缓存最新
weak_ptr
: 新对象的生命周期由调用者通过shared_ptr
管理。- 当所有
shared_ptr
都析构时,对象释放,缓存的weak_ptr
过期。
优点:
- 避免重复加载耗时资源。
- 对象生命周期由
shared_ptr
自动管理,缓存不会导致内存泄漏。 - 访问缓存时能自动感知对象是否仍然有效。
观察者模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <vector>
#include <memory>
class Observer {
public:
virtual ~Observer() = default;
virtual void onNotify(int data) = 0;
};
class Subject {
// 用 weak_ptr 存储观察者,避免拥有它们的生命周期
std::vector<std::weak_ptr<Observer>> observers_;
public:
void addObserver(std::shared_ptr<Observer> obs) {
observers_.push_back(obs);
}
void notify(int data) {
// 遍历所有观察者,尝试提升 weak_ptr
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto obs = it->lock()) {
obs->onNotify(data); // 有效观察者,调用通知
++it;
} else {
// 观察者已销毁,移除过期的 weak_ptr
it = observers_.erase(it);
}
}
}
};
// 具体观察者实现
class ConcreteObserver : public Observer, public std::enable_shared_from_this<ConcreteObserver> {
public:
void onNotify(int data) override {
std::cout << "Observer notified with data: " << data << "\n";
}
};
int main() {
Subject subject;
{
auto obs1 = std::make_shared<ConcreteObserver>();
subject.addObserver(obs1);
subject.notify(42); // 通知,有效观察者会收到
} // obs1 作用域结束,析构,观察者销毁
subject.notify(100); // 此时观察者已销毁,通知时会跳过并清理过期 weak_ptr
return 0;
}
Subject
持有std::weak_ptr<Observer>
,不影响Observer
生命周期。notify()
时通过lock()
提升为shared_ptr
,确保访问安全。- 当需要调用观察者方法(如
onNotify
)时,不能直接用weak_ptr
,因为它可能指向已经被销毁的对象(悬空)。 - 如果观察者对象还存在,
lock()
返回有效的shared_ptr
,这时引用计数+1,保证对象在这段代码里不被销毁。
- 当需要调用观察者方法(如
- 如果观察者已销毁,
lock()
返回空指针,Subject
会移除过期的观察者指针。 - 这样既避免了悬空访问,也避免了循环引用导致内存泄漏。
打破循环引用
场景:A ↔ B,A 持有 B,B 弱引用 A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <memory>
struct B; // 前置声明
struct A {
std::shared_ptr<B> b_ptr; // 拥有B的所有权
~A() { std::cout << "A destroyed\n"; }
};
struct B {
std::weak_ptr<A> a_ptr; // 弱引用A,避免循环引用
~B() { std::cout << "B destroyed\n"; }
};
int main() {
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A拥有B
b->a_ptr = a; // B弱引用A,不增加引用计数
// 两个对象相互关联,但不会循环引用导致泄漏
} // 作用域结束,a和b都正确销毁
return 0;
}
如果用 shared_ptr
循环引用(错误示范)
1
2
3
4
struct B {
std::shared_ptr<A> a_ptr; // 强引用A,循环引用
~B() { std::cout << "B destroyed\n"; }
};
这会导致
A
和B
互相持有强引用,引用计数都不为零,即使超出作用域,析构函数也不会被调用,造成内存泄漏。
结论
- 用
weak_ptr
替代其中一个方向的shared_ptr
,打破循环引用。 - 这样,资源才能被正确释放,不会泄漏。
注意事项
std::weak_ptr
不是“弱化版”shared_ptr
,而是用于观察(observe)资源而非管理资源。expired()
检查非原子,只应与lock()
搭配使用。weak_ptr
本身也增加控制块的“弱引用计数”,管理其自身生命周期。
总结
用
weak_ptr
替代可能悬空的shared_ptr
。典型应用:缓存、观察者列表、打破
shared_ptr
循环引用。用
lock()
原子地检测有效性并获取shared_ptr
。