欢迎大家访问我的个人主页guts的小屋
循环引用是学习智能指针过程中的一个小难点,笔者愚钝,明明知道是两个指针互相引用导致了内存泄漏,但看各种文字资料时,脑子里总是一团浆糊,感觉似懂非懂,于是自己绘制了几张图片,思路和概念才清晰起来,希望这篇博客能帮助有同样困惑的同学解惑。
shared_ptr
的实现
要明白循环引用,我们首先需要知道shared_ptr
是如何实现的,它内部包含两个指针,一个指向我们要管理的资源对象,另一个指向则指向这个对象所对应的控制块。在这个控制块中,包含了这个对象的强引用计数,当我们第一次声明一个shared_ptr
变量时,就会为这个资源对象分配一个控制块,并将强引用计数初始化为1。
当有新的shared_ptr
对象拥有这个资源的时候,强引用计数就会+1,当shared_ptr
对象销毁或者调用.reset()
方法时,强引用计数就会-1,当强引用计数为0时,这个资源就会析构。这样我们就通过RAII来避免了内存的泄露。
循环引用
这看起来是一个很完美的设计,但是shared_ptr
管理的资源和shared_ptr
对象本身是两块内存,并且shared_ptr
管理的资源本身也可以包含一个shared_ptr
对象并持有资源,这样就会导致,一旦两个资源本身互相持有,那么他们析构的条件就是对方析构,这就会导致内存的泄露。
这么说可能有些模糊,我们直接来看例子:
class A {
public:std::shared_ptr<A> ptr;...
}
int main() {// code Istd::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<A> b = std::make_shared<A>(); // code IIa->ptr = b;b->ptr = a;return 0;
}
code I
运行完毕后,我们会在堆中创建两个A
类的对象,我们将其称之为OBJ_A
和OBJ_B
,同时栈上会有两个std::shared_ptr<A>
对象a
和b
,他们分别指向OBJ_A
和OBJ_B
以及他们各自的控制块CB_A
和CB_B
,如下图所示
此时两个控制块的引用计数都是1,当我们运行Code II
后,OBJ_A
和OBJ_B
会拥有对对方的所有权,他们的引用计数都变为2
这个时候代码运行完毕,a
和b
作为栈上的对象被销毁,OBJ_A
和OBJ_B
的引用计数都下降为1,但是由于此时他们都持有指向彼此的shared_ptr
对象,引用计数仍为1,不会继续下降,这块内存就不会被释放,造成了内存泄露。
weak_ptr
的引入
为了解决这个问题,我们引入了weak_ptr
,它不拥有资源的所有权,指向资源的时候只会增加弱引用计数而非强引用计数,弱引用计数不决定资源的生命周期,只决定控制块的生命周期,当弱引用计数为0时销毁控制块。
需要注意的是,shared_ptr
也会让弱引用计数+1。
class A {
public:std::weak_ptr<A> ptr;...
}
int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<A> b = std::make_shared<A>();a->ptr = b;b->ptr = a;return 0;
}
当我们使用上面的代码时,几个对象的声明周期如下图所示:
在b
和a
离开作用域后
-
b
析构,CB_B
的强引用计数-1为0,弱引用计数-1=1 -
OBJ_B
析构,CB_A
的弱引用计数-1=1 -
a
析构,CB_A
的强引用计数-1为0,弱引用计数-1=0 -
OBJ_A
和CB_A
析构,CB_B
的弱引用计数-1=0 -
CB_B
析构