C++中std::shared_ptr的循环引用如何通过weak_ptr解决? (引用计数陷阱)

9次阅读

std::shared_ptr循环引用会导致内存泄漏,因引用计数无法归零;应使用weak_ptr打破循环,仅一方用shared_ptr持有,另一方用weak_ptr观察,并通过lock()安全访问。

C++中std::shared_ptr的循环引用如何通过weak_ptr解决? (引用计数陷阱)

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

当两个 std::shared_ptr 相互持有对方管理的对象时,引用计数永远无法归零,对象永远不会析构。这不是 bug,是设计使然——shared_ptr 只看“还有谁在用”,不关心“谁在用谁”。

典型场景:双向链表节点、父子对象(如树节点)、观察者与被观察者之间用 shared_ptr 互相保存。

现象:程序结束前内存占用持续增长,Valgrind 或 ASan 报告“still reachable”块,对象的析构函数根本不执行。

用 weak_ptr 打断循环中的一个引用链

weak_ptr 不增加引用计数,只“观察”对象是否还活着。它必须配合 lock() 升级为 shared_ptr 才能安全访问对象,且升级失败时返回空 shared_ptr

立即学习C++免费学习笔记(深入)”;

关键原则:循环中**只有一方用 shared_ptr 持有,另一方改用 weak_ptr**。比如父节点用 shared_ptr 持有子节点,子节点用 weak_ptr 回指父节点。

  • 不要在构造函数里直接用 weak_ptrlock() 结果初始化成员 shared_ptr,容易引发未定义行为
  • 访问前必须调用 lock() 并检查返回值,不能假设对象一定存在
  • weak_ptr 本身不参与资源管理,析构开销极小,但频繁 lock() 有原子操作成本

示例:

struct Node {     std::shared_ptr<Node> next;     std::weak_ptr<Node> prev; // 不再用 shared_ptr };  auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; b->prev = a; // 此时 b 不延长 a 的生命周期

weak_ptr::lock() 返回空 shared_ptr 的常见原因

不是所有 weak_ptr 失效都意味着你写错了,但得知道为什么失效:

  • 被观察的 shared_ptr 已全部析构(正常)
  • 对象在线程中被另一个线程释放,而本线程的 weak_ptr 还没来得及 lock()(需同步)
  • 误把临时 shared_ptr 赋给 weak_ptr,比如 wptr = std::make_shared<t>();</t> 后立刻离开作用域,临时对象销毁

错误写法:

std::weak_ptr<int> wp; {     auto sp = std::make_shared<int>(42);     wp = sp; // OK } // sp 析构 → wp now expired auto sp2 = wp.lock(); // sp2.get() == nullptr

std::enable_shared_from_this 是弱引用的辅助工具

当你需要从对象内部安全地生成指向自身的 shared_ptr(比如回调、异步任务),直接用 this 构造会破坏引用计数——此时必须继承 std::enable_shared_from_this<t></t>

它内部持有一个 weak_ptr,由首次创建的 shared_ptr 初始化。所以:

  • 必须通过 shared_ptr 创建对象(如 make_shared),否则 shared_from_this()std::bad_weak_ptr
  • weak_from_this()c++17 新增,可直接拿到那个内部 weak_ptr,适合做延迟访问或观察
  • 不要在构造函数里调用 shared_from_this(),此时内部 weak_ptr 还没被初始化

典型误用:

struct Good : std::enable_shared_from_this<Good> {     void go() {         auto self = shared_from_this(); // OK,前提是 this 来自 shared_ptr     } };  // 错误: Good g; g.shared_from_this(); // 抛异常

循环引用真正的复杂点不在语法,而在逻辑所有权的划分:哪个模块“真正拥有”这个对象?weak_ptr 不是补丁,是所有权模型的显式声明。一旦开始用,就得全程按这个契约来——比如子节点访问父节点前,必须接受“父节点可能已不存在”这个事实。

text=ZqhQzanResources