2023年11月16日
Last updated on

C++ 智能指针:weak_ptr 入门指南

作者:


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

先运行一个生命周期例子

保存为 weak_ptr_demo.cpp,用支持 C++17 的编译器运行:

#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';
}
Terminal window
clang++ -std=c++17 -Wall -Wextra -pedantic weak_ptr_demo.cpp -o weak_ptr_demo
./weak_ptr_demo

输出为:

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

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

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

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

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

expired() 和 use_count() 适合观察状态,不能替代 lock()。尤其在多线程里,检查时存在的对象可能在检查后失去最后一个所有者;lock() 原子地完成“获取共享所有权或返回空值”,成功后无需再检查 expired()。C++ 标准草案:lock

不过,持有对象只解决生命周期问题。多个线程同时修改对象内容,仍然需要互斥或原子操作;shared_ptr 的引用计数保护不等于对象内部数据自动线程安全。可以接着看 std::atomic 笔记。

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

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

#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

接下来读什么

这篇属于 C++ 与工具链导读。代码能编译但编辑器无法识别时,先看 compile_commands.json 的字段与生成方式,再配置 clangd 与 VS Code。