🚩前言
自C++11起,有三大智能指针:
unique_ptr
shared_ptr
weak_ptr
都是内存管理中的非常重要的一部分动态内存管理 - cppreference.com。
其中shared_ptr
在实际应用中具有非常广泛的应用。
而由于其较unique_ptr的功能多,有引用计数的概念。导致存在一个名为循环引用的问题。这是一个非常经典的坑。但解决方案也不是很复杂。下面来一起看看吧。
🚩循环引用
🕹️例子1
Code
#include <iostream>
#include <memory>
struct A;
struct B;
struct A {
std::shared_ptr<B> ptr;
A() {
std::cout << __func__ << std::endl;
}
~A() {
std::cout << __func__ << std::endl;
}
};
struct B {
std::shared_ptr<A> ptr;
B() {
std::cout << __func__ << std::endl;
}
~B() {
std::cout << __func__ << std::endl;
}
};
int main() {
auto pa = std::make_shared<A>();
auto pb = std::make_shared<B>();
pa->ptr = pb;
pb->ptr = pa;
}
😭shared_ptr (错误)
此代码,分别有两个类,class A
和class B
。其中都有一个成员都是零一个类型的shared_ptr。
分别构造好对象后都去执行另一个对象,这就产生了著名的循环引用问题。
下面是输出结果,未能执行析构函数,导致内存泄漏
# 输出结果
A
B
逐步分析
当两个对象构造完毕后,两块share_ptr的智能指针的引用计数都为1,且内部的智能指针对象是默认空构造。
接着让两个内部的shared_ptr都指向对方,此时引用计数都为2。
此后,脱离作用域,开始RAII回收资源。
此时两个在main函数中的shared_ptr对象都回收,两个引用计数都-1,变为1。
然后,就没有然后了。因此引用计数不为0,导致实际的对象内存不符合智能指针的要求,无法释放。
😂weak_ptr (正确)
假如此时将class B
的智能指针改为weak_ptr。即可解决该问题。
weak_ptr其不会增加智能指针的引用计数,当实际需要使用的时候再构造出shared_ptr。这就是一种视图的作用。
逐步分析
当两个对象构造完毕后,两块share_ptr的智能指针的引用计数都为1,且内部的智能指针对象是默认空构造。
然后,B的weak指向A,A的引用计数块不变;A的share指向B,B的引用计数+1。Bcnt=2
,Acnt=1
。
此后,脱离作用域,开始RAII回收资源。
此时两个在main函数中的shared_ptr对象都回收,Bcnt=1
,Acnt=0
。
此时A符合回收条件,A资源释放,在A中的share也释放,B的计数-1=0。Bcnt=0
然后B也符合释放条件,B释放。
最终资源全部正确回收。
😭unique_ptr (错误)
我们再做一个假设,如果把class B
的智能指针改为unique_ptr
,会发生什么。
先公布答案:
A
B
~A
~B
~A
当然主函数也要略做修改,因为shared_ptr不能直接转化为unique_ptr。
int main() {
auto pa = std::make_shared<A>();
auto pb = std::make_shared<B>();
pa->ptr = pb;
pb->ptr.reset(pa.get());
}
当然看到这里,敏感的朋友会直接发现这根本就是一种错误的编码。因为pa的对象直接让两个可以掌管生命周期的对象把控了。
所以,这就出现了A对象的两次析构。
这里的原理分析与上面类似,这里就不做过多展开了。
🕹️例子2
Code
除了定义两个不同的类,有可能会产生循环引用,一个类自身也是有可能会产生的。
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> ptr;
Node() {
std::cout << __func__ << std::endl;
}
~Node() {
std::cout << __func__ << std::endl;
}
};
int main() {
auto p = std::make_shared<Node>();
p->ptr = p;
}
这里解决方案一样,将内部的shared_ptr改为weak_ptr即可。
而要是这里改为unique_ptr
时,有趣的事情就发生了,这里直接无限递归调用析构函数。
因为,当shared_ptr对象回收时,引用计数归0,导致Node对象回收。
当析构函数结束后,类内的unique_ptr也是回收资源,它指向的也是当前对象,因此再次delete,也就是执行析构函数,析构成员。
自此不断递归。。。
🚩END
关注我,学习更多C/C++,算法,计算机知识
B站:
👨💻主页:天赐细莲 bilibili