15. shared_ptr:循环引用问题、weak_ptr引入
shared_ptr 用起来很爽,对吧?自动释放内存,引用计数归零就销毁。但爽归爽,它有个坑——循环引用。我在项目中就栽过跟头,今天咱们把这个坑填平。
15.1 循环引用:shared_ptr 的阿喀琉斯之踵
先看一个经典场景。两个对象互相持有对方的 shared_ptr:
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 循环引用!
// 离开作用域,a 和 b 都不会被销毁
return 0;
}
运行一下,你会发现 "Node destroyed" 根本没打印。内存泄漏了。
为什么会这样?
a 的引用计数是 2(a 自己 + b->next)。b 的引用计数也是 2(b 自己 + a->next)。离开作用域时,a 和 b 的局部变量销毁,引用计数各减 1。但 a 的计数还剩 1(被 b 指着),b 的计数还剩 1(被 a 指着)。谁都不肯先死,结果谁都不死。
核心问题:两个 shared_ptr 互相持有,形成环状依赖。引用计数永远到不了零。
我个人习惯把这种问题叫做「死锁式泄漏」——不是没人管,而是互相管着,谁也脱不了身。
15.2 解决方案:weak_ptr 登场
weak_ptr 是什么?说白了,它是一个「弱引用」——它指向对象,但不增加引用计数。它像是一个观察者,能看到对象,但不影响对象的生命周期。
用 weak_ptr 改写上面的例子:
#include <memory>
#include <iostream>
struct Node {
std::weak_ptr<Node> next; // 改成 weak_ptr
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // weak_ptr 不增加计数
// 离开作用域,a 和 b 正常销毁
return 0;
}
这次 "Node destroyed" 打印了两次。问题解决。
原理很简单:weak_ptr 不参与引用计数。a 的计数只有 1(a 自己),b 的计数只有 1(b 自己)。离开作用域,各自归零,正常销毁。
15.3 weak_ptr 的正确使用姿势
weak_ptr 不能直接访问对象。你得先把它「升级」成 shared_ptr。为什么?因为对象可能已经被销毁了。
std::shared_ptr<Node> sp = std::make_shared<Node>();
std::weak_ptr<Node> wp = sp;
// 错误:不能直接解引用 weak_ptr
// wp->doSomething(); // 编译错误
// 正确做法:先 lock() 升级
if (auto locked = wp.lock()) {
locked->doSomething(); // 安全访问
} else {
std::cout << "对象已销毁\n";
}
lock() 返回一个 shared_ptr。如果对象还在,返回有效指针;如果对象已销毁,返回空指针。这个检查是必须的,别偷懒。
小技巧:lock() 是原子操作,线程安全。多线程环境下放心用。
15.4 什么时候用 weak_ptr?
我总结了几种典型场景:
- 父子关系:父对象持有子对象的 shared_ptr,子对象持有父对象的 weak_ptr。避免循环。
- 缓存/观察者模式:缓存池持有对象的 weak_ptr,不延长对象生命周期。对象销毁后,缓存自动失效。
- 资源池:比如连接池,池中持有 weak_ptr,外部使用者持有 shared_ptr。用完了自动释放。
举个例子,一个简单的观察者模式:
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify() {
for (auto& wp : observers) {
if (auto sp = wp.lock()) {
sp->update();
}
}
}
};
这里用 weak_ptr 的好处是:观察者销毁后,notify 时自动跳过,不会访问野指针。
15.5 避坑指南
我曾经在项目里犯过一个低级错误:用 weak_ptr 之后忘了 lock(),直接调用成员函数。编译没过,但当时我还纳闷——明明 weak_ptr 指向的对象还在啊。后来才意识到,weak_ptr 本身不提供直接访问,这是设计上的安全考量。
还有一次,我在多线程环境下用 weak_ptr,lock() 之后对象被另一个线程销毁了。嗯,lock() 是原子操作,但 lock() 返回的 shared_ptr 一旦拿到,对象就不会被销毁了——因为 shared_ptr 增加了引用计数。所以只要 lock() 成功,后续操作就是安全的。
警告:不要用 weak_ptr 的 expired() 判断对象是否存活,然后直接访问。因为 expired() 返回 true 后,对象可能立刻被销毁。正确做法是:直接 lock(),检查返回值。
15.6 性能小贴士
weak_ptr 的 lock() 有开销。它需要原子操作来增加引用计数。如果频繁 lock/unlock,性能会受影响。我建议:
- 如果确定对象在某个生命周期内一定存活,可以先 lock() 一次,把 shared_ptr 存起来反复用。
- 避免在热点循环中反复 lock()。
- 能用原始指针的地方,别硬上 weak_ptr。但要注意原始指针没有生命周期保障。
说白了,weak_ptr 是 shared_ptr 的「安全补丁」,不是万能药。该用的时候用,不该用的时候别滥用。
15.7 本章小结
循环引用是 shared_ptr 的固有缺陷。weak_ptr 就是专门来治这个病的。记住三点:
- weak_ptr 不增加引用计数,不延长生命周期。
- 使用前必须 lock(),检查返回值。
- 适用于父子关系、缓存、观察者等场景。
嗯,到这里,shared_ptr 和 weak_ptr 的配合使用你应该心里有数了。下次遇到循环引用,别慌,上 weak_ptr 就对了。
一句话总结:shared_ptr 负责生命周期,weak_ptr 负责打破循环。两者配合,天下无敌。