44. 智能指针(shared_ptr与weak_ptr):引用计数、循环引用、weak_ptr
说实话,智能指针这个话题,我每次给团队新人讲的时候,都会多说几句。因为很多人觉得它就是个“自动释放内存的工具”,用起来很简单。但实际项目里,坑可不少。
今天咱们就聊聊 shared_ptr 和 weak_ptr。这两个东西,一个管“共享所有权”,一个管“弱引用”。搞懂了它们,你的C++代码会安全很多。
shared_ptr:引用计数的核心
shared_ptr 的原理其实不复杂。每个 shared_ptr 对象内部都维护着一个“引用计数”。当你拷贝一个 shared_ptr 时,计数加1;当某个 shared_ptr 被销毁时,计数减1。当计数降到0时,它管理的对象就会被自动删除。
我习惯把引用计数想象成“有多少人正在使用这个资源”。没人用了,资源就该被回收。
#include <memory>
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Resource created\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void doWork() { std::cout << "Working...\n"; }
};
int main() {
std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();
std::cout << "use_count: " << ptr1.use_count() << "\n"; // 1
{
std::shared_ptr<Resource> ptr2 = ptr1;
std::cout << "use_count: " << ptr1.use_count() << "\n"; // 2
} // ptr2 离开作用域,计数减1
std::cout << "use_count: " << ptr1.use_count() << "\n"; // 1
return 0;
} // ptr1 离开作用域,计数为0,Resource被销毁
你看,use_count() 会告诉你当前有多少个 shared_ptr 指向同一个对象。这个值从1变到2,再变回1,最后归零时对象被销毁。
小提示: 尽量用 std::make_shared 而不是直接 new。它更安全,而且内存分配效率更高。我见过不少因为直接用 new 导致异常安全问题的代码。
循环引用:shared_ptr 的致命陷阱
好了,重点来了。shared_ptr 有个大坑——循环引用。说白了就是两个对象互相持有对方的 shared_ptr,导致引用计数永远降不到0,内存泄漏了。
我在项目中遇到过这种问题。一个双向链表,每个节点用 shared_ptr 指向下一个节点,也指向前一个节点。结果程序跑着跑着,内存占用越来越高。查了半天才发现是循环引用。
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;
// a 和 b 的引用计数都是2
// a 被 a 和 b->prev 持有
// b 被 b 和 a->next 持有
// 离开作用域时,计数减1,但不会归零
// 所以 Node 永远不会被销毁
std::cout << "a use_count: " << a.use_count() << "\n"; // 2
std::cout << "b use_count: " << b.use_count() << "\n"; // 2
return 0;
} // 没有输出 "Node destroyed"
为什么会这样?因为 a 和 b 互相引用,形成了一个环。每个节点的引用计数都是2。当 main 函数结束时,a 和 b 这两个 shared_ptr 被销毁,计数减1,但还剩下1。这个“1”就是对方持有的引用。所以两个对象都活得好好的,谁也释放不了谁。
警告: 循环引用是 shared_ptr 最常见的坑。如果你发现程序有内存泄漏,而且用了 shared_ptr,第一个怀疑对象就是循环引用。
weak_ptr:打破循环的钥匙
那怎么解决呢?用 weak_ptr。weak_ptr 是一种“弱引用”,它不会增加引用计数。它只是“观察”一个 shared_ptr 管理的对象,但不参与所有权管理。
说白了,weak_ptr 就像是一个“旁观者”。它知道对象是否存在,但不会阻止对象被销毁。
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 用 weak_ptr 代替 shared_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->prev = a; // weak_ptr 不会增加引用计数
std::cout << "a use_count: " << a.use_count() << "\n"; // 1
std::cout << "b use_count: " << b.use_count() << "\n"; // 1
return 0;
} // 两个 Node 都会被销毁
你看,把 prev 改成 weak_ptr 后,引用计数变成了1。循环被打破了,对象可以正常释放。
如何使用 weak_ptr
weak_ptr 不能直接访问对象。你需要先调用 lock() 方法,它会返回一个 shared_ptr。如果对象已经被销毁,返回的 shared_ptr 是空的。
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
// 通过 lock() 获取 shared_ptr
if (auto locked = wp.lock()) {
std::cout << "Value: " << *locked << "\n";
} else {
std::cout << "Object has been destroyed\n";
}
sp.reset(); // 释放对象
if (auto locked = wp.lock()) {
std::cout << "Value: " << *locked << "\n";
} else {
std::cout << "Object has been destroyed\n";
}
return 0;
}
嗯,这里要注意:lock() 是线程安全的。但如果你在 lock() 之后、使用对象之前,有其他线程销毁了对象,那怎么办?其实不用担心,因为 lock() 返回的 shared_ptr 会增加引用计数,所以对象在你使用期间不会被销毁。
知识体系总览
下面这张图,是我梳理的 shared_ptr 和 weak_ptr 的核心逻辑。你看一眼就能明白它们的关系。
实际应用场景
我总结了几种常见的使用场景,你可以参考一下:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 树形结构(父→子) | 父持有子:shared_ptr子持有父: weak_ptr |
避免循环引用,子节点不控制父节点生命周期 |
| 观察者模式 | 被观察者持有观察者:shared_ptr观察者持有被观察者: weak_ptr |
观察者不应阻止被观察者销毁 |
| 缓存系统 | 缓存持有对象:shared_ptr外部使用者: weak_ptr |
缓存控制生命周期,外部只临时使用 |
| 双向链表 | next:shared_ptrprev: weak_ptr |
打破循环,让节点可以正常释放 |
核心原则: 在对象关系中,明确谁是“所有者”,谁只是“使用者”。所有者用 shared_ptr,使用者用 weak_ptr。这样既安全又清晰。
避坑指南
我曾经在一个项目里,看到有人把 weak_ptr 当 shared_ptr 用。他以为 weak_ptr 会自动升级成 shared_ptr,结果每次 lock() 都返回空指针。嗯,这其实是对 weak_ptr 的误解。
还有一次,我在代码审查时发现,有人用 shared_ptr 管理一个临时对象,然后在回调里又创建了一个 shared_ptr 指向同一个对象。结果引用计数乱套了。后来我建议他用 enable_shared_from_this 来解决。
最后说一句:shared_ptr 不是万能的。如果你不需要共享所有权,用 unique_ptr 更轻量。只有当你确实需要多个指针共享同一个对象时,才考虑 shared_ptr 和 weak_ptr。