shared_ptr循环引用必然导致内存泄漏,因强引用计数不归零则析构不触发,双方互持使计数恒为1;须将非拥有关系一端改为weak_ptr并lock()安全访问。

shared_ptr循环引用为什么一定导致泄漏
因为析构只发生在强引用计数归零时,而两个对象互相用shared_ptr持有对方,就等于各自给对方“搭了一只手”——哪怕外部所有shared_ptr都离开了作用域,它们内部的引用仍让计数卡在1,析构函数永远不会执行。
典型现象是:程序跑一段时间后内存持续上涨,valgrind或LeakSanitizer报告对象未释放,但use_count()查出来不是0;对象的析构日志完全不打印。
- 树节点中
parent和children都用shared_ptr - 观察者模式里被观察者存了观察者的
shared_ptr - lambda捕获
[self = shared_from_this()]又反过来被对象持有
weak_ptr不是“替代品”,而是“关系声明”
weak_ptr本身不解决泄漏,它只是把“我拥有你”改成“我可能用到你,但不决定你生死”。关键动作是:把非拥有关系那一端明确换成weak_ptr,再配合lock()安全访问。
比如父子结构中,子节点回引父节点属于典型的非拥有关系——父节点销毁,子节点不该跟着活下来;反之,父节点管理子节点生命周期,必须用shared_ptr。
- 改写
Node::parent为std::weak_ptr<node></node>,保留Node::children为std::shared_ptr<node></node> - 使用前必须调用
parent.lock(),返回shared_ptr或空指针,不能直接解引用weak_ptr -
expired()只是快速检查,真正要用还得lock(),否则存在竞态(对象可能在检查后、使用前被析构)
lock()之后别忘判空,这是最常漏掉的一步
weak_ptr::lock()返回的是shared_ptr,但它可能是空的——说明目标对象已经被析构。跳过这步直接解引用,行为未定义,轻则崩溃,重则静默错误。
正确写法不是if (wp.expired()) return;然后直接用wp.lock().get(),而是:
if (auto sp = wp.lock()) {
// 安全使用 sp
sp->do_something();
} else {
// 对象已销毁,按业务逻辑处理(如清理缓存、重试、返回错误)
}
- 不要反复调用
lock()多次,每次都会重新尝试升级,开销略高;一次获取,多次使用 - 如果只是读状态且不关心对象是否还活着,用
expired()够快;但凡要调用成员函数,必须lock() - 在多线程场景下,
lock()是原子的,但拿到的shared_ptr只保证它指向的对象在该shared_ptr生命周期内有效
容器里存shared_ptr也要小心反向引用
很多人以为只要用了shared_ptr就安全了,结果在std::vector<:shared_ptr>></:shared_ptr>里塞了一堆对象,其中某些对象又通过shared_ptr回指容器本身(比如回调注册),照样形成跨容器的循环引用。
更隐蔽的是闭包捕获:lambda里捕获[ptr = shared_from_this()]并注册进某个长期存活的服务,服务又把lambda存进自己的shared_ptr容器——这时候ptr和该服务就互持了。
- 回调场景优先考虑
weak_ptr捕获:[weak_self = weak_from_this()]() { if (auto self = weak_self.lock()) { ... } } - 避免在容器元素内部持有指向容器本身的
shared_ptr,改用索引、ID 或外部管理的weak_ptr -
std::vector<:unique_ptr>></:unique_ptr>比std::vector<:shared_ptr>></:shared_ptr>更不容易出循环引用,除非你主动把它传给别的对象
循环引用不是代码写错了,而是所有权关系没想清楚。用weak_ptr不是加个修饰符就完事,得先回答一个问题:“这个指针,是决定对方生死的关键一票,还是只在需要时临时借个光?”——答案决定了该用shared_ptr还是weak_ptr,也决定了lock()之后敢不敢直接用。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











