# C++ 智能指针：weak_ptr 入门指南

> 用可运行的 C++17 示例理解 weak_ptr、lock 的强引用持有语义，以及如何打破 shared_ptr 循环引用。

作者：[汤国放](https://tonwork.fun/#person)
发布日期：2023-11-16
更新日期：2026-10-08
原文：https://tonwork.fun/blog/c-%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88-weak_ptr-%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/

---

`std::weak_ptr` 用来观察 `shared_ptr` 管理的对象，但不参与对象的强引用计数。真正访问对象时，调用 `lock()` 得到一个 `shared_ptr`：成功后，这个局部强引用会让对象至少活到它被释放为止。把这两件事区分开，才不会把“弱引用不延长生命周期”误读成“访问时也不持有对象”。[C++ 标准草案：weak_ptr observers](https://eel.is/c++draft/util.smartptr.weak.obs)

## 先运行一个生命周期例子

保存为 `weak_ptr_demo.cpp`，用支持 C++17 的编译器运行：

```cpp
#include <iostream>
#include <memory>

int main() {
    auto owner = std::make_shared<int>(42);
    std::weak_ptr<int> observer = owner;
    std::cout << "owners=" << observer.use_count() << '\n';

    {
        auto held = observer.lock();
        owner.reset();
        // held 仍持有对象，所以释放原 owner 并不会销毁它。
        std::cout << "held=" << *held << '\n';
        std::cout << "expired=" << observer.expired() << '\n';
    }

    // held 离开作用域，最后一个强引用消失。
    std::cout << "expired=" << observer.expired() << '\n';
    std::cout << "lock=" << static_cast<bool>(observer.lock()) << '\n';
}
```

```sh
clang++ -std=c++17 -Wall -Wextra -pedantic weak_ptr_demo.cpp -o weak_ptr_demo
./weak_ptr_demo
```

输出为：

```text
owners=1
held=42
expired=0
expired=1
lock=0
```

这里 `weak_ptr` 不增加强引用计数，但控制块仍需要保留，以便判断对象是否过期。对象被销毁与控制块被释放不是同一个时刻。

## 访问时用 lock，不要先 expired 再裸指针访问

日常写法是让拿到的强引用覆盖整个访问区间：

```cpp
if (auto held = observer.lock()) {
    std::cout << *held << '\n';
} else {
    std::cout << "对象已经释放\n";
}
```

`expired()` 和 `use_count()` 适合观察状态，不能替代 `lock()`。尤其在多线程里，检查时存在的对象可能在检查后失去最后一个所有者；`lock()` 原子地完成“获取共享所有权或返回空值”，成功后无需再检查 `expired()`。[C++ 标准草案：lock](https://eel.is/c++draft/util.smartptr.weak.obs#lib:weak_ptr,lock)

不过，持有对象只解决生命周期问题。多个线程同时修改对象内容，仍然需要互斥或原子操作；`shared_ptr` 的引用计数保护不等于对象内部数据自动线程安全。可以接着看 [std::atomic 笔记](https://tonwork.fun/blog/c-atomic/)。

## 用 weak_ptr 表达“能找到，但不拥有”

父子关系里，父节点可以拥有子节点，而子节点只观察父节点。如果两边都用 `shared_ptr`，它们会互相保活，外部所有者消失后也无法释放。

```cpp
#include <iostream>
#include <memory>

struct Node {
    explicit Node(int value) : id(value) {}
    int id;
    std::shared_ptr<Node> child;
    std::weak_ptr<Node> parent;

    ~Node() { std::cout << "destroy " << id << '\n'; }
};

int main() {
    auto root = std::make_shared<Node>(1);
    root->child = std::make_shared<Node>(2);
    root->child->parent = root;

    if (auto parent = root->child->parent.lock()) {
        std::cout << "parent=" << parent->id << '\n';
    }
}
```

同样用上面的 C++17 命令编译，程序退出时会输出 `destroy 1` 和 `destroy 2`。这里要先想清楚谁负责所有权：不能为了消除循环，就把所有关系都改成弱引用，那样对象可能过早销毁。缓存、异步回调和观察者列表也适用这条原则。

`reset()` 让弱引用不再观察任何对象；需要在容器中按所有权关系排序时，可以考虑 `owner_less`，它比较的是共享所有权关系，而不是对象值。[C++ 标准草案：owner_less](https://eel.is/c++draft/util.smartptr.ownerless)

## 接下来读什么

这篇属于 [C++ 与工具链导读](https://tonwork.fun/topics/cpp-toolchain/)。代码能编译但编辑器无法识别时，先看 [compile_commands.json 的字段与生成方式](https://tonwork.fun/blog/compile_commands-json-%E7%AE%80%E6%98%93%E7%A7%91%E6%99%AE%E4%B8%8E%E7%BC%96%E8%AF%91%E9%80%89%E9%A1%B9%E8%A7%A3%E9%87%8A/)，再配置 [clangd 与 VS Code](https://tonwork.fun/blog/%E4%BB%A3%E7%A0%81%E8%87%AA%E5%8A%A8%E6%8F%90%E7%A4%BA-clangd-vscode%E5%85%A5%E9%97%A8/)。
