[C++智能指针] weak_ptr
weak_ptr
当两个对象互相持有对方的shared_ptr时它们的引用计数永远不会归零,weak_ptr就是为了解决问题而生的,它是一种不参与引用计数的观察者指针,可以通过它来查看对象是否还活着,如果活着就临时获取一个shared_ptr来访问,但它本身不会延长对象的生命周期。
weak_ptr的API
lock()
lock()尝试获取一个指向对象的shared_ptr,如果对象仍然存在(强引用计数>0),则返回一个有效的shared_ptr,如果对象已经被销毁(强引用计数=0),返回一个空的shared_ptr(nullptr)。lock()是线程安全的,这意味着在多线程环境下,多个线程可以同时调用lock(),标准保证返回的shared_ptr要么指向一个有效对象,要么为空。
lock()的作用是:把”弱引用”临时提升为”强引用”,从而保证在你使用对象期间,它不会被销毁。
而它之所以要增加强引用计数,正是为了保证这段”使用期间”对象一定存活——这是
weak_ptr存在的根本意义。
weak_ptr 是 shared_ptr 的观察者,它:
- 不参与对象的生命周期管理(不增加
strong_count)。 - 只观察对象是否还存在。
- 因此,
weak_ptr本身不能直接访问对象(没有operator*,没有operator->)。
为什么不能直接访问? 因为 weak_ptr 不拥有对象,对象随时可能被销毁。如果你能直接 *weak,就可能在对象已经销毁后访问悬空内存。
1
auto locked = weak.lock()
lock() 的行为可以概括为:
- 检查对象是否还存活(即
strong_count > 0)。 - 如果存活:原子地把
strong_count+1,返回一个指向该对象的shared_ptr。 - 如果已销毁:返回一个空的
shared_ptr(nullptr)。
expired()
expired()返回一个bool值,表示对象是否已经被销毁,不过实际使用时,推荐直接使用lock(),而不是先检查再lock(),因为在多线程环境下,expired()返回false之后到调用lock()直接,对象可能已经被另一个线程销毁,这会导致竞态条件。lock()一次性完成了检查和增加引用计数两个操作,避免这个问题。
use_count()
use_count()返回当前指向对象的shared_ptr数量(强引用计数)。和expired()一样,返回值在使用时可能已经过时了,所以一般用于调试。
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
#include <memory>
#include <iostream>
void weak_ptr_api_demo() {
std::weak_ptr<int> weak;
{
auto shared = std::make_shared<int>(42);
weak = shared; // weak 不增加引用计数
std::cout << "use_count: " << weak.use_count() << "\n"; // 1
std::cout << "expired: " << weak.expired() << "\n"; // 0 (false)
// 通过 lock() 获取 shared_ptr
if (auto locked = weak.lock()) {
std::cout << "value: " << *locked << "\n"; // 42
std::cout << "use_count after lock: "
<< weak.use_count() << "\n"; // 2
}
// locked 离开作用域,引用计数回到 1
}
// shared 已经被销毁
std::cout << "expired after scope: " << weak.expired() << "\n"; // 1 (true)
// lock() 返回空的 shared_ptr
auto locked = weak.lock();
std::cout << "locked is nullptr: " << (locked == nullptr) << "\n"; // 1 (true)
}
weak_ptr 不能直接解引用——无法写 *weak 或 weak->member。必须先通过 lock() 获取 shared_ptr,然后通过 shared_ptr 访问对象。这个设计是故意的:weak_ptr 是一种”不确定对象是否还存在”的引用,直接访问太危险了。lock() 的原子检查保证了您获取到的 shared_ptr 要么指向一个活着的对象,要么是空的——不会出现”获取到指针但对象已经被删”的悬垂指针问题。
weak_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
struct NodeFixed {
std::string name;
std::shared_ptr<NodeFixed> next;
std::weak_ptr<NodeFixed> prev; // 改为 weak_ptr
explicit NodeFixed(const std::string& n) : name(n) {
std::cout << "Node(" << name << ") 构造\n";
}
~NodeFixed() {
std::cout << "~Node(" << name << ") 析构\n";
}
};
void fixed_circular_reference() {
auto a = std::make_shared<NodeFixed>("A");
auto b = std::make_shared<NodeFixed>("B");
a->next = b; // A → B(B 的强引用计数: 1 → 2)
b->prev = a; // B ⇢ A(弱引用,A 的强引用计数不变,仍然是 1)
std::cout << "准备离开函数...\n";
// 函数结束时:
// a 离开作用域,A 的强引用计数: 1 → 0,A 被销毁
// A 的析构会销毁 A->next,B 的强引用计数: 2 → 1
// b 离开作用域,B 的强引用计数: 1 → 0,B 被销毁
// 所有节点都被正确释放!
}
这里的关键在于b->prev = a这一行,weak_ptr不增加a的强引用计数,所以当局部变量a离开作用域时,a的强引用计数从1直接降为0.
这个模式可以推广到任何有”父子关系”或”上下游关系”的数据结构:强引用方向用 shared_ptr(持有所有权),弱引用方向用 weak_ptr(仅观察,不持有所有权)。只要图中不存在全是强引用的环,引用计数就能正常工作。