文章

[C++智能指针] weak_ptr

[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() 的行为可以概括为:

  1. 检查对象是否还存活(即 strong_count > 0)。
  2. 如果存活:原子地把 strong_count +1,返回一个指向该对象的 shared_ptr。
  3. 如果已销毁:返回一个空的 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(仅观察,不持有所有权)。只要图中不存在全是强引用的环,引用计数就能正常工作。

本文由作者按照 CC BY 4.0 进行授权