C++里的std::shared_ptr如何解决循环引用?(配合使用weak_ptr辅助指针)

std::shared_ptr循环引用会导致内存泄漏,因引用计数无法归零;需用std::weak_ptr打破强引用链,其不增引用计数,须通过lock()获取临时shared_ptr访问对象。

std::shared_ptr 循环引用会导致内存泄漏

当两个 std::shared_ptr 相互持有对方所管理的对象时,引用计数永远无法归零,对象不会被析构 —— 这就是循环引用。它不报错、不崩溃,但内存持续增长,是典型的“静默泄漏”。

std::weak_ptr 打断强引用链

std::weak_ptr 不增加引用计数,只“观察”对象是否还活着。它不能直接访问对象,必须通过 lock() 转成 std::shared_ptr 才能使用;若原对象已销毁,lock() 返回空的 std::shared_ptr

常见做法是:一方用 std::shared_ptr 拥有另一方,另一方用 std::weak_ptr 回指——比如父类持子类的 std::shared_ptr,子类持父类的 std::weak_ptr

struct Parent;
struct Child {
    std::weak_ptr parent; // 不参与所有权,不增加引用计数
};

struct Parent {
    std::shared_ptr child;
};

容易踩的坑:误用 weak_ptr::lock() 或直接构造

  • 直接用 std::shared_ptr(parent) 构造(即拷贝构造)会触发未定义行为 —— std::weak_ptr 不能直接转 std::shared_ptr,必须调用 lock()
  • lock() 返回的是临时 std::shared_ptr,如果只用于条件判断但没保存,后续再访问可能已失效
  • 在析构函数里调用 lock() 是安全的,但若此时对象正被销毁(比如 Parent 析构中访问 child->parent.lock()),lock() 会返回空,这是预期行为

调试循环引用的实用技巧

没有编译期检查,只能靠设计约束和运行时辅助:

  • use_count() 打印关键节点的引用数(仅 debug 版),比如 ptr.use_count() 看是否异常偏高
  • 避免在类内部用 shared_from_this() 向自己创建循环,尤其在回调注册场景下
  • 考虑用 RAII 容器(如 std::vector<:shared_ptr>>)替代双向链表式结构,减少手动管理指针的必要

最麻烦的地方不是写 weak_ptr,而是判断哪边该强、哪边该弱——这取决于对象生命周期的自然归属关系,一旦定错,问题会藏得很深。