循环引用是智能指针使用中的常见陷阱,尤其在std::shared_ptr的配合下会导致内存泄漏。下面从根本原理、典型场景和解决方案三方面系统剖析并给出最佳实践。
一、循环引用的根源
- 引用计数:std::shared_ptr 通过引用计数实现自动释放,当计数归零时才析构对象。
- 互相引用:如果对象 A 的 std::shared_ptr 指向对象 B,B 又通过 std::shared_ptr 指向 A,双方计数永不为零,导致两者始终驻留内存。
二、典型场景
- 父子关系:父节点保存子节点的 std::shared_ptr,子节点持有父节点的 std::shared_ptr。
- 双向链表:节点 A 的 next 指向 B,B 的 prev 指向 A。
- 图结构:节点间存在复杂的相互引用。
三、解决策略
-
使用 std::weak_ptr
- 弱引用:不计入引用计数,避免循环。
- 用法:当 A 需要访问 B 时,用
std::weak_ptr获取临时std::shared_ptr:std::weak_ptr <Node> parent; // 父节点 // 在需要时 if (auto p = parent.lock()) { // p 现在是 shared_ptr } - 父子案例:父节点使用
std::shared_ptr保存子节点;子节点用std::weak_ptr保存父节点。
-
设计更清晰的责任链
- 单向关联:尽量让引用单向流动,避免双向持有。
- 解耦:通过事件回调、观察者模式或依赖注入减少直接引用。
-
使用 std::unique_ptr
- 所有权单一:若对象间不存在共享所有权,可改用
std::unique_ptr。 - 传递所有权:在需要时使用
std::move将所有权转移,天然防止循环。
- 所有权单一:若对象间不存在共享所有权,可改用
-
自定义删除器
- 在特殊情况下,可通过自定义删除器配合
shared_ptr进行复杂销毁逻辑,但要谨慎使用。
- 在特殊情况下,可通过自定义删除器配合
四、代码示例
#include <memory>
#include <iostream>
struct Node {
int id;
std::shared_ptr <Node> next; // 单向指向下一个
std::weak_ptr <Node> prev; // 弱引用指向前一个
Node(int i) : id(i) { std::cout << "Node " << id << " constructed\n"; }
~Node() { std::cout << "Node " << id << " destroyed\n"; }
};
int main() {
auto n1 = std::make_shared <Node>(1);
auto n2 = std::make_shared <Node>(2);
auto n3 = std::make_shared <Node>(3);
// 建立双向链表
n1->next = n2; n2->prev = n1;
n2->next = n3; n3->prev = n2;
// 结束时自动销毁,无泄漏
return 0;
}
输出
Node 1 constructed
Node 2 constructed
Node 3 constructed
Node 3 destroyed
Node 2 destroyed
Node 1 destroyed
可见,使用 weak_ptr 使前驱不计数,链表完整销毁。
五、常见误区
- 忽略
weak_ptr的锁定:.lock()可能返回空指针,需判断。 - 混用
unique_ptr与shared_ptr:在同一对象上混用可能导致析构时出现多重删除。 - 错误的生命周期管理:在多线程场景中,仍需考虑线程安全,使用
std::shared_mutex或std::atomic保护。
六、总结
- 理解引用计数:循环引用导致计数永不归零。
- 优先使用
weak_ptr:在需要引用但不拥有所有权的地方。 - 简化设计:尽量单向引用,或用
unique_ptr断开所有权。
只要在设计初期就预见到可能的循环引用并合理利用 std::weak_ptr 与 std::unique_ptr,就能避免 C++ 中最常见的智能指针泄漏问题。