C++中
shared_ptr导致的循环依赖,本质上是对象间相互持有强引用,导致引用计数永远无法归零,从而造成内存泄漏。解决这个问题的核心方案是引入
weak_ptr,它提供了一种非拥有性的引用,能够打破循环。
当我们谈论C++的智能指针,尤其是
shared_ptr时,它无疑是管理动态内存的一把利器。它通过引用计数机制,确保对象在不再被任何
shared_ptr引用时自动释放。然而,这套机制并非万无一失,它有一个著名的陷阱——循环依赖(或称循环引用)。说实话,我个人第一次遇到这个问题时,着实困惑了一阵子,代码逻辑看起来都没错,但内存就是不释放。
解决方案
shared_ptr循环依赖的发生,通常是因为两个或多个对象通过
shared_ptr相互持有对方的引用。想象一下A对象有一个
shared_ptr指向B,同时B对象也有一个
shared_ptr指向A。当外部对A和B的
shared_ptr都失效后,A的引用计数因为B的存在而不会降到0,B的引用计数也因为A的存在而不会降到0。它们就像两个互相抱紧溺水的人,谁也无法放手,最终一同沉没,导致内存泄漏。
解决之道,就是引入
weak_ptr。
weak_ptr是一种“弱引用”智能指针,它不增加对象的引用计数。你可以把它理解为一种观察者,它能“看”到对象,但不会“拥有”对象。当所有
shared_ptr都释放后,即便还有
weak_ptr指向该对象,对象也会被正确销毁。
weak_ptr的强大之处在于,它提供了一个
lock()方法,可以尝试获取一个
shared_ptr。如果对象仍然存在(即至少有一个
shared_ptr还在引用它),
lock()会返回一个有效的
shared_ptr;否则,它会返回一个空的
shared_ptr。
以下是一个经典的循环依赖示例及其
weak_ptr解决方案:
#include <iostream> #include <memory> #include <string> class B; // 前向声明 class A { public: std::shared_ptr<B> b_ptr; std::string name; A(const std::string& n) : name(n) { std::cout << "A " << name << " constructed." << std::endl; } ~A() { std::cout << "A " << name << " destructed." << std::endl; } void set_b(std::shared_ptr<B> b) { b_ptr = b; } }; class B { public: // 循环依赖问题:这里如果也是 shared_ptr<A> a_ptr; 就会形成循环 // 解决方案:使用 weak_ptr<A> std::weak_ptr<A> a_ptr; std::string name; B(const std::string& n) : name(n) { std::cout << "B " << name << " constructed." << std::endl; } ~B() { std::cout << "B " << name << " destructed." << std::endl; } void set_a(std::shared_ptr<A> a) { a_ptr = a; } void use_a() { if (auto sharedA = a_ptr.lock()) { // 尝试获取 shared_ptr std::cout << "B " << name << " is using A " << sharedA->name << std::endl; } else { std::cout << "B " << name << ": A is no longer available." << std::endl; } } }; void create_circular_dependency() { std::shared_ptr<A> a = std::make_shared<A>("Alpha"); std::shared_ptr<B> b = std::make_shared<B>("Beta"); // 建立相互引用 a->set_b(b); b->set_a(a); // 这里B持有A的weak_ptr std::cout << "A's ref count: " << a.use_count() << std::endl; // 此时为1 (因为b_ptr持有) std::cout << "B's ref count: " << b.use_count() << std::endl; // 此时为1 (因为a_ptr持有) b->use_a(); // B可以安全地使用A } // a 和 b 在这里离开作用域,shared_ptr 被销毁 int main() { create_circular_dependency(); std::cout << "End of main function." << std::endl; // 如果没有使用 weak_ptr,A和B的析构函数将不会被调用,造成内存泄漏。 // 使用 weak_ptr 后,A和B会正确析构。 return 0; }
运行上述代码,你会看到A和B的析构函数被正确调用,表明内存得到了释放。关键在于,当
create_circular_dependency函数结束,
a和
b这两个
shared_ptr离开作用域时,它们所持有的对象的引用计数会减一。对于
a对象,它的引用计数降为0(因为
b_ptr持有的是
shared_ptr<B>,而
b持有的是
weak_ptr<A>,
weak_ptr不增加引用计数),
a被销毁。
a销毁后,其内部的
b_ptr也会被销毁,导致
b的引用计数降为0,
b也被销毁。这样,循环就被完美打破了。 C++
shared_ptr循环引用究竟是如何发生的?
要真正理解
weak_ptr的巧妙,我们得先深挖一下
shared_ptr循环引用的根源。这并不是
shared_ptr设计上的缺陷,而是它“共享所有权”语义的自然结果。每个
shared_ptr内部都维护着一个控制块(control block),这个控制块存储着两个计数器:一个是强引用计数(use_count),记录有多少个
shared_ptr指向该对象;另一个是弱引用计数(weak_count),记录有多少个
weak_ptr指向该对象。
当一个
shared_ptr被创建或复制时,强引用计数增加。当
shared_ptr被销毁或重新赋值时,强引用计数减少。只有当强引用计数降到零时,被管理的对象才会被销毁。
循环引用就发生在两个或多个对象彼此“强拥有”对方的时候。 举个例子:
class Parent; class Child; class Parent { public: std::shared_ptr<Child> child; Parent() { std::cout << "Parent constructed." << std::endl; } ~Parent() { std::cout << "Parent destructed." << std::endl; } }; class Child { public: std::shared_ptr<Parent> parent; // 问题所在:这里是 shared_ptr Child() { std::cout << "Child constructed." << std::endl; } ~Child() { std::cout << "Child destructed." << std::endl; } }; void create_problem() { std::shared_ptr<Parent> p = std::make_shared<Parent>(); std::shared_ptr<Child> c = std::make_shared<Child>(); p->child = c; // Parent持有Child,Child的强引用计数变为2 (p->child 和 c) c->parent = p; // Child持有Parent,Parent的强引用计数变为2 (c->parent 和 p) std::cout << "Parent ref count: " << p.use_count() << std::endl; // 输出 2 std::cout << "Child ref count: " << c.use_count() << std::endl; // 输出 2 } // p 和 c 离开作用域
当
create_problem函数执行完毕,局部变量
p和
c被销毁。

全面的AI聚合平台,一站式访问所有顶级AI模型


p
被销毁,Parent
对象的强引用计数从2降到1(因为c->parent
还在持有)。c
被销毁,Child
对象的强引用计数从2降到1(因为p->child
还在持有)。 此时,Parent
和Child
对象的强引用计数都为1,谁都无法降到0。这意味着它们所指向的内存永远不会被释放,即使它们已经无法从程序中被访问到,形成了内存泄漏。这就是shared_ptr
循环引用的发生机制。它不是错误,而是shared_ptr
强所有权语义在特定场景下的一个副作用。
weak_ptr是如何解决循环依赖的,以及它有哪些使用上的注意事项?
weak_ptr解决循环依赖的核心机制,在于它不参与对象的强引用计数。它仅仅是“观察”对象是否存在,而不会影响对象的生命周期。当一个
weak_ptr被创建时,它只会增加对象的弱引用计数(weak_count),这个计数只用于判断控制块是否可以被销毁,而不是对象本身。只要对象的强引用计数不为零,它就不会被销毁。
使用
weak_ptr时,最关键的一点是,你不能直接通过
weak_ptr访问它所指向的对象。你需要先通过
weak_ptr::lock()方法,尝试获取一个
shared_ptr。
- 如果对象仍然存在(即至少有一个
shared_ptr
在引用它),lock()
会返回一个有效的shared_ptr
。你可以像使用普通shared_ptr
一样安全地访问对象。 - 如果对象已经被销毁(所有
shared_ptr
都已释放),lock()
会返回一个空的shared_ptr
(即nullptr
)。这时,你必须检查返回的shared_ptr
是否为空,以避免访问已销毁的内存,这是一种非常重要的安全机制。
使用注意事项:
-
务必检查
lock()
的返回值: 这是weak_ptr
使用的黄金法则。weak_ptr
所指向的对象随时可能被销毁,因此在使用前必须通过if (auto shared_obj = weak_ptr_instance.lock()) { ... }
这样的结构来确保对象仍然有效。 -
选择正确的“弱”边: 在设计对象关系时,需要仔细考虑哪一方应该持有弱引用。通常,拥有者持有
shared_ptr
,被拥有者或者观察者持有weak_ptr
。-
父子关系: 如果父对象拥有子对象,子对象需要访问父对象但不能影响父对象的生命周期,那么子对象应该持有父对象的
weak_ptr
。例如,一个Node
持有其children
的shared_ptr
,而children
则持有Parent
的weak_ptr
。 -
观察者模式: 在观察者模式中,被观察者通常持有观察者的
weak_ptr
。这样,当观察者自身生命周期结束时,它就可以被安全销毁,而不会因为被观察者持有强引用而造成泄漏。 -
缓存: 缓存系统有时会使用
weak_ptr
来引用缓存项。如果一个缓存项没有其他强引用,它就可以被垃圾回收,即使缓存本身还“记得”它。
-
父子关系: 如果父对象拥有子对象,子对象需要访问父对象但不能影响父对象的生命周期,那么子对象应该持有父对象的
-
weak_ptr
的开销:weak_ptr
的创建、复制和销毁都会操作控制块,lock()
方法也需要一定的开销。但这些开销通常很小,在大多数应用中可以忽略不计。过度担心性能而避免使用weak_ptr
,可能导致更严重的内存泄漏问题。 -
weak_ptr
不能直接解引用: 记住,weak_ptr
本身不提供operator*
或operator->
。它只是一个句柄,必须先提升为shared_ptr
才能使用。
weak_ptr,还有其他避免
shared_ptr循环引用的策略吗?
虽然
weak_ptr是解决
shared_ptr循环依赖最标准、最推荐的方案,但在某些情况下,我们也可以从设计层面去规避这个问题。这往往需要我们重新审视对象间的关系和所有权语义。
-
重新设计所有权关系: 这是最根本的策略。很多时候,循环依赖的出现,可能暗示着对象模型本身存在一些不清晰或不合理之处。
-
单向所有权: 问问自己,两个对象真的都需要“拥有”对方吗?是否可以将其中的一个关系改为单向引用?例如,A拥有B,B知道A的存在但并不拥有A(即B内部持有A的裸指针或
weak_ptr
)。 -
明确的层次结构: 在树形或图状结构中,尽量建立明确的父子关系,让父节点拥有子节点,子节点通过
weak_ptr
或裸指针(在生命周期明确受控的情况下)引用父节点。 -
引入中间管理者: 有时,可以将相互引用的两个对象A和B的共同管理职责抽离到一个第三者C。由C持有A和B的
shared_ptr
,而A和B之间则只通过裸指针或weak_ptr
进行通信。这样,C负责它们的生命周期,A和B则避免了直接的强引用循环。
-
单向所有权: 问问自己,两个对象真的都需要“拥有”对方吗?是否可以将其中的一个关系改为单向引用?例如,A拥有B,B知道A的存在但并不拥有A(即B内部持有A的裸指针或
-
使用裸指针(极度谨慎): 在某些非常特殊且生命周期严格受控的场景下,可以考虑使用裸指针来打破循环。但这种做法风险极高,因为它完全放弃了智能指针提供的安全性。你必须100%确定:
- 被裸指针指向的对象在其生命周期内不会被提前销毁。
- 裸指针绝不会被用于删除对象。
- 裸指针的使用范围和时间都非常有限。 这种方法通常只适用于内部实现细节,且有明确的注释和文档说明。对于初学者或大多数应用场景,强烈不建议使用。
-
事件/回调机制: 当对象之间需要相互通信但又不想建立直接的强引用时,可以考虑事件或回调机制。
- 例如,A需要知道B的状态变化,而不是直接持有B的
shared_ptr
。B可以提供一个注册回调的接口,A通过这个接口注册一个lambda函数或成员函数。当B状态变化时,它调用这些回调。这里的关键是,B在存储这些回调时,如果回调涉及到A的成员函数,B应该存储一个std::function
,并且这个std::function
内部捕获的this
指针应该是weak_ptr<A>
的lock()
结果,或者干脆只存储一个不捕获A的this
的普通函数指针。
- 例如,A需要知道B的状态变化,而不是直接持有B的
总的来说,
weak_ptr是C++标准库为解决
shared_ptr循环引用提供的优雅且安全的方案。而其他策略更多的是从设计思想上进行规避,它们在某些特定场景下可能更合适,但通常也伴随着更高的设计复杂性或潜在的风险。在实际开发中,优先考虑
weak_ptr,如果发现
weak_ptr导致代码结构复杂或不自然,再回头审视对象间的关系是否可以简化。
以上就是C++shared_ptr与循环依赖问题解决方法的详细内容,更多请关注知识资源分享宝库其它相关文章!
相关标签: node ai c++ ios 解决方法 作用域 标准库 red if 成员函数 析构函数 auto 局部变量 循环 Lambda 指针 接口 operator function 对象 作用域 事件 this 大家都在看: C++井字棋AI实现 简单决策算法编写 如何为C++搭建边缘AI训练环境 TensorFlow分布式训练配置 怎样用C++开发井字棋AI 简单决策算法实现方案 怎样为C++配置嵌入式AI开发环境 TensorFlow Lite Micro移植指南 C++井字棋游戏怎么开发 二维数组与简单AI逻辑实现
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。