16. weak_ptr:弱引用、lock()、expired()
聊完了 shared_ptr,咱们得面对一个现实问题——循环引用。我记得刚入行那会儿,第一次在项目里大规模用 shared_ptr,自我感觉良好,觉得内存管理终于解放了。结果跑压力测试的时候,内存曲线一路往上飙,最后 OOM 了。排查了半天,发现是几个对象互相持有 shared_ptr,谁也释放不了。
嗯,这时候 weak_ptr 就派上用场了。
弱引用到底是什么?
说白了,weak_ptr 是一个「旁观者」。它指向一个由 shared_ptr 管理的对象,但不增加引用计数。它不拥有这个对象,只是「看着」它。
你想想看,如果 A 和 B 互相持有 shared_ptr,那它们的引用计数永远到不了 0,谁也析构不了。这就是典型的循环引用死锁。用 weak_ptr 打破这个环,让其中一方只观察、不拥有,问题就解决了。
核心区别:
- shared_ptr:共享所有权,引用计数 +1
- weak_ptr:无所有权,引用计数不变
lock():从观察到使用
weak_ptr 不能直接访问对象。你想用它指向的对象?得先调用 lock()。这个方法会返回一个 shared_ptr。如果对象还活着,返回有效的 shared_ptr;如果对象已经被释放了,返回空 shared_ptr。
我个人习惯在需要访问 weak_ptr 指向的对象时,先 lock 一下,然后判断是否为空。这样既安全又清晰。
#include <memory>
#include <iostream>
class Observer {
public:
void observe(std::weak_ptr<int> wp) {
// 尝试获取 shared_ptr
if (auto sp = wp.lock()) {
std::cout << "对象还活着,值为: " << *sp << std::endl;
} else {
std::cout << "对象已经被释放了" << std::endl;
}
}
};
int main() {
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
Observer obs;
obs.observe(wp); // 输出:对象还活着,值为: 42
sp.reset(); // 释放 shared_ptr
obs.observe(wp); // 输出:对象已经被释放了
return 0;
}
你看,lock() 返回的 shared_ptr 在 if 语句里临时持有对象,用完了自动释放。这个临时持有很重要——它保证你在访问对象期间,对象不会被其他线程释放掉。
小技巧:lock() 是原子操作。它检查对象是否存活,如果存活就增加引用计数,整个过程是线程安全的。我在多线程项目里经常用这个特性。
expired():先检查再使用?
expired() 用来检查 weak_ptr 指向的对象是否已经被释放。返回 true 表示对象没了,false 表示还活着。
但是——这里有个坑。我曾经在代码里这么写过:
// 错误示范
if (!wp.expired()) {
auto sp = wp.lock(); // 这里可能已经过期了!
// 使用 sp...
}
为什么有问题?因为 expired() 检查完到 lock() 之间,可能有另一个线程把 shared_ptr 释放了。expired() 返回 false,但 lock() 却返回了空指针。
所以我建议:直接用 lock(),别先调 expired()。lock() 自己会做原子检查,更安全。
注意:不要用 expired() + lock() 的组合。直接用 lock() 判断返回值就够了。这是我在代码审查里经常指出的问题。
实战:用 weak_ptr 打破循环引用
来个经典例子——父子节点互相引用。
#include <memory>
#include <vector>
#include <iostream>
class Child;
class Parent {
public:
std::vector<std::shared_ptr<Child>> children;
~Parent() { std::cout << "Parent 析构" << std::endl; }
};
class Child {
public:
std::weak_ptr<Parent> parent; // 用 weak_ptr 避免循环引用
~Child() { std::cout << "Child 析构" << std::endl; }
};
int main() {
{
auto p = std::make_shared<Parent>();
auto c = std::make_shared<Child>();
p->children.push_back(c);
c->parent = p; // weak_ptr 赋值,不增加引用计数
std::cout << "离开作用域..." << std::endl;
}
// 两个对象都能正常析构
std::cout << "都析构完了" << std::endl;
return 0;
}
运行结果:
离开作用域...
Parent 析构
Child 析构
都析构完了
如果 Child 里用的是 shared_ptr,Parent 和 Child 就互相牵着不放,谁也析构不了。用 weak_ptr,Parent 释放后,Child 里的 weak_ptr 自动过期,完美。
weak_ptr 的典型应用场景
| 场景 | 说明 |
|---|---|
| 缓存系统 | 缓存持有对象的 weak_ptr,对象被其他地方释放后,缓存自动失效 |
| 观察者模式 | 观察者持有被观察者的 weak_ptr,避免观察者阻止被观察者析构 |
| 树形结构 | 子节点用 weak_ptr 指向父节点,父节点用 shared_ptr 持有子节点 |
| 线程安全访问 | 用 weak_ptr 传递对象引用,接收方通过 lock() 安全获取临时所有权 |
知识体系:weak_ptr 核心逻辑
下面这张图帮你理清 weak_ptr 和 shared_ptr 的关系:
使用建议
- 能用 weak_ptr 就别用裸指针——weak_ptr 至少能告诉你对象还在不在,裸指针做不到。
- lock() 是首选——别搞 expired() + lock() 的组合拳,一步到位最安全。
- 临时持有——lock() 返回的 shared_ptr 用完了就释放,别长期持有,否则 weak_ptr 的意义就没了。
- 缓存场景——我习惯在缓存里存 weak_ptr,配合 lock() 判断缓存是否有效,比手动管理生命周期省心多了。
个人经验:我在一个网络库项目里用 weak_ptr 管理连接对象。每个连接由 shared_ptr 持有,各种回调里存 weak_ptr。回调触发时 lock() 一下,如果连接还在就处理,不在了就忽略。这样既避免了循环引用,又防止了野指针。用了好几年,没出过内存问题。
weak_ptr 不是什么复杂的东西,但它解决了一个很实际的问题。你想想看,没有它,循环引用就得靠手动释放或者裸指针来搞,那多容易出 bug。记住一句话:shared_ptr 负责拥有,weak_ptr 负责观察。各司其职,内存安全就有保障。