循环引用指两个对象的shared_ptr相互持有,导致引用计数无法归零而内存泄漏;weak_ptr不增加引用计数,可打破循环,通过lock()安全访问对象,常用于父子关系或双向链表中避免内存泄漏。
在C++中,shared_ptr通过引用计数管理对象生命周期,但当两个或多个对象互相持有对方的shared_ptr时,会形成循环引用,导致内存无法释放。这时就需要用到weak_ptr来打破循环。
什么是循环引用?
考虑两个类A和B,每个类都持有一个指向对方的shared_ptr:
struct B; // 前向声明 struct A { std::shared_ptr<B> ptr; ~A() { std::cout << "A destroyedn"; } }; struct B { std::shared_ptr<A> ptr; ~B() { std::cout << "B destroyedn"; } };
如果这样使用:
auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->ptr = b; b->ptr = a;
此时a和b的引用计数都是2。离开作用域后,shared_ptr会减少引用计数到1,但由于仍大于0,析构函数不会被调用,造成内存泄漏。
立即学习“C++免费学习笔记(深入)”;
weak_ptr如何解决循环引用?
weak_ptr是shared_ptr的观察者,它不增加引用计数。它可以指向一个由shared_ptr管理的对象,但不会阻止对象被销毁。
修改上面的例子,把其中一个shared_ptr换成weak_ptr:
struct B; struct A { std::shared_ptr<B> ptr; ~A() { std::cout << "A destroyedn"; } }; struct B { std::weak_ptr<A> ptr; // 改为 weak_ptr ~B() { std::cout << "B destroyedn"; } };
现在即使相互引用,也不会形成循环。当外部的shared_ptr离开作用域,引用计数正确归零,对象能被正常释放。
如何安全使用weak_ptr?
由于weak_ptr不保证所指对象一定存在,访问前必须检查:
- 使用 lock() 获取临时的shared_ptr,若对象已销毁则返回空
- 使用 expired() 判断对象是否已被释放(但有竞态风险)
推荐方式:
std::shared_ptr<A> temp = b.ptr.lock(); if (temp) { // 安全使用 temp std::cout << "Object is aliven"; } else { std::cout << "Object has been destroyedn"; }
这样做既打破了循环引用,又能安全地访问目标对象。
基本上就这些。在设计有父子关系、双向链表或观察者模式等结构时,记得让从属方使用weak_ptr,主导方使用shared_ptr,就能有效避免内存泄漏。