shared_ptr循环引用导致内存泄漏,通过weak_ptr打破循环。示例中A强引用B,B弱引用A,避免了析构时引用计数无法归零的问题,确保对象正确销毁。

shared_ptr循环引用是C++内存管理中一个常见的陷阱,它会导致对象无法被正确销毁,进而引发内存泄漏。解决这个问题的核心思路是打破对象间强引用的循环,通常通过引入
weak_ptr来将循环中的一个强引用替换为弱引用。
weak_ptr不增加对象的引用计数,允许对象在没有其他强引用时被正常销毁,从而避免泄漏。
#include#include #include // 前向声明,因为A和B互相引用 class B; class A { public: std::string name; std::shared_ptr b_ptr; // A强引用B A(std::string n) : name(n) { std::cout << "A " << name << " constructor\n"; } ~A() { std::cout << "A " << name << " destructor\n"; } void set_b(std::shared_ptr b) { b_ptr = b; } }; class B { public: std::string name; std::weak_ptr a_ptr; // B弱引用A,这是打破循环的关键 B(std::string n) : name(n) { std::cout << "B " << name << " constructor\n"; } ~B() { std::cout << "B " << name << " destructor\n"; } void set_a(std::shared_ptr a) { a_ptr = a; } void check_a_status() { if (auto shared_a = a_ptr.lock()) { // 尝试提升为shared_ptr std::cout << "B " << name << " observes A " << shared_a->name << " is still alive.\n"; } else { std::cout << "B " << name << " observes A is gone.\n"; } } }; int main() { std::cout << "--- 示例开始 ---\n"; { std::shared_ptr pa = std::make_shared("Object_A"); std::shared_ptr pb = std::make_shared("Object_B"); std::cout << "初始引用计数:pa=" << pa.use_count() << ", pb=" << pb.use_count() << "\n"; // 建立引用关系 pa->set_b(pb); // A强引用B,B的引用计数增加 pb->set_a(pa); // B弱引用A,A的引用计数不变 std::cout << "建立引用后:pa=" << pa.use_count() << ", pb=" << pb.use_count() << "\n"; // 此时,pa的use_count应为1 (main函数持有),pb的use_count应为2 (main函数持有,A对象内部持有) pb->check_a_status(); // B尝试访问A,此时A应该还活着 } // pa和pb超出作用域,智能指针自动析构 std::cout << "--- 示例结束 ---\n"; // 如果没有循环引用,A和B的析构函数都会被调用 return 0; }
为什么我的shared_ptr
对象迟迟不销毁?深入理解循环引用是如何形成的
我们经常会遇到这样的情况:你用
shared_ptr管理资源,觉得万无一失了,结果程序跑着跑着,内存占用就上去了,那些本该被释放的对象却还在内存里躺着。这多半就是循环引用在作祟。
它的形成其实很简单,就是两个或多个对象,它们之间相互持有对方的
shared_ptr。比如,A对象有一个
shared_ptr成员,同时B对象也有一个
shared_ptr成员。当它们各自初始化并相互引用后,A的引用计数会因为B持有它而增加,B的引用计数也会因为A持有它而增加。
想象一下这个过程: 你创建了一个
shared_ptr pa,此时A的引用计数是1。 接着你创建了一个
shared_ptr pb,B的引用计数也是1。 然后,你让
pa内部的成员指向
pb。这时,B的引用计数从1变成了2(
pb持有一次,
pa内部持有一次)。 再接着,你让
pb内部的成员指向
pa。这时,A的引用计数从1变成了2(
pa持有一次,
pb内部持有一次)。
现在问题来了,当
pa和
pb这两个局部变量超出作用域时,它们会尝试销毁自己持有的
shared_ptr。
pa销毁,A的引用计数从2变成1。
pb销毁,B的引用计数从2变成1。但注意,A的内部仍然持有一个
shared_ptr指向B,B的内部也仍然持有一个
shared_ptr指向A。这意味着A和B的引用计数永远不会降到0。它们会一直“互相指着对方”,谁也无法先走一步,最终导致内存泄漏。
立即学习“C++免费学习笔记(深入)”;
这就像两个人手拉着手站在悬崖边,谁也松不开,因为一旦松开,对方就可能掉下去。但实际上,只要有一个人愿意放手,或者至少不是用“死死抓住”的方式握着,这个僵局就能打破。这种“死死抓住”就是
shared_ptr的强引用特性,它确保只要有一个
shared_ptr存在,对象就不会被销毁。
深入浅出weak_ptr
:它是如何巧妙地打破循环引用的?
weak_ptr,正如其名,是一个“弱”指针。它不拥有对象的所有权,也不会增加对象的引用计数。这正是它能打破循环引用的关键所在。你可以把它理解成一个“观察者”或者“旁观者”,它只是知道某个对象可能存在,但它不参与对象的生命周期管理。
当我们用
weak_ptr替换循环引用中的一个
shared_ptr时,比如在上面的A和B的例子中,让B持有
weak_ptr而不是
shared_ptr。那么,整个引用链条就会变成这样:
pa
持有shared_ptr
,A的引用计数为1。pb
持有shared_ptr
,B的引用计数为1。pa
内部持有shared_ptr
,B的引用计数变为2。pb
内部持有weak_ptr
。注意,weak_ptr
不会增加A的引用计数,所以A的引用计数仍然是1。
现在,当
pa和
pb超出作用域时:
pa
销毁,A的引用计数从1降到0。此时,A对象被销毁。pb
销毁,B的引用计数从2降到1(只剩下pa
内部持有的那个)。
等A被销毁后,
pb内部的那个
weak_ptr就会自动失效(
expired()方法会返回true)。 然后,当
pa内部的
shared_ptr也超出作用域(或者说A对象被销毁时,其成员
b_ptr也会被销毁),B的引用计数从1降到0。B对象也被销毁。
看,这样一来,A和B都能被正常销毁了。
weak_ptr就像是循环中的一个“软连接”,它允许你访问对象,但不会阻碍对象的销毁。当你需要使用
weak_ptr指向的对象时,你必须先调用它的
lock()方法,尝试将其提升为一个
shared_ptr。如果对象仍然存在,
lock()会返回一个有效的
shared_ptr;如果对象已经被销毁了,它会返回一个空的
shared_ptr。这提供了一种安全地访问可能已被销毁对象的方式,避免了悬空指针的问题。
这种机制在父子关系、观察者模式或者缓存管理中非常有用。比如一个父节点拥有子节点,子节点需要知道它的父节点是谁,但子节点不应该拥有父节点,否则就会形成循环。这时,子节点持有父节点的
weak_ptr就是非常自然且正确的选择。
除了打破循环引用,weak_ptr
还有哪些实用的应用场景?
weak_ptr的作用远不止打破循环引用这么简单,它在很多场景下都能发挥独特的优势,主要体现在需要“观察”但不“拥有”对象所有权的需求上。
一个很典型的场景就是观察者模式(Observer Pattern)。在观察者模式中,一个主题(Subject)对象会维护一个观察者(Observer)列表,并在状态改变时通知所有观察者。如果主题持有观察者的
shared_ptr,而观察者又反过来持有主题的
shared_ptr(比如为了获取主题的信息),那么就会形成循环引用。正确的做法是,主题持有观察者的
weak_ptr。这样,当一个观察者不再被其他地方强引用时,它就可以被销毁,而主题并不会阻止它的销毁。主题在通知观察者时,只需要尝试
lock()这个
weak_ptr,如果成功,就说明观察者还活着,可以通知;如果失败,说明观察者已经“去世”了,可以将其从列表中移除。
另一个常见应用是缓存(Cache)机制。假设你有一个对象缓存,里面存放着一些昂贵的对象。你希望这些对象在没有其他地方使用它们时能够被自动清理,以节省内存。如果缓存直接持有这些对象的
shared_ptr,那么只要对象在缓存里,它的引用计数就不会降到0,永远不会被清理。这时,缓存就应该持有这些对象的
weak_ptr。当外部代码需要某个对象时,它可以通过缓存获取一个
shared_ptr。当所有外部
shared_ptr都失效后,对象就会被销毁,缓存中的
weak_ptr也会随之失效。下次请求时,缓存发现
weak_ptr失效了,就知道这个对象已经被清理,可以重新创建或










