智能指针的死穴 -- 循环引用
C++最新标准C++11中已将基于引用计数的智能指针share_prt收入囊中,智能指针的使用门槛越来越低,不需要使用boost库,我们也能轻松享受智能指针给我们带来的方便。
智能指针,正如它的名字一样,似乎是个近乎完美的聪明角色,程序员不用再纠结于new出来的内存在哪释放比较合适这种问题。比如当一个资源被多个模块共享时,程序员需要在所有模块的生命周期都结束时,由最后一个不使用该指针的模块触发指针的释放行为,而模块的生命周期可能根本在写代码时就确定不了。
智能指针的出现,给不支持垃圾回收机制的C++带来了一丝曙光。下面简单介绍一下智能指针的运行机制:
当我们需要从堆上申请空间时,可以将new出来的指针交由智能指针管理,比如:shared_ptr<int> a(new int);,这样当a出作用域时,在a对象析构的时候,就会释放持有的堆上指针,这是通过C++的析构函数实现的。
当一个智能指针对象拷贝赋值给另外一个智能指针时,比如shared_ptr<int> b = a;a和b两个智能指针指向了同一块堆上的空间,a或b中的任意一个对象出作用域时,都不应该释放堆上的空间,因为还有另外一个智能指针对象在引用这个堆空间,于是就引入了引用计数机制来解决这个问题。当一个智能指针对象被创建时,会在堆上创建一个用于计数的空间,当shared_ptr<int> b = a;执行后,b对象浅拷贝a对象的计数区指针,然后将计数区的值+1。这样就相当于拷贝赋值出的一组智能指针都指向同一块堆上的数据空间,同时还共享另外一块堆上计数区(这也是叫做shared_ptr的原因)。在智能指针对象析构时,不是简单的直接释放持有的堆数据空间,而是先将共享的引用计数-1,之后发现引用计数为0的话,才调用delete。
智能指针的实现思路也体现了C++基于对象的原则,对象应该为自己管理的资源负责,包括资源的分配与释放,而且最好将资源的释放与分配搞的自动化一点,典型的实现方法就是在构造函数里分配资源,在析构函数里释放资源,这样当其他程序员在使用这个对象时,该对象的资源问题几乎不用额外的操心,即优雅又方便。
好啦,我是华丽的分割线。下面进入本文的重点,当循环引用发生时,基于计数的共享机制将会被彻底击败。
一个简单的例子,分析见注释:(下面代码一运行,由于内存有泄漏,内存使用量会暴涨,大家小心测试哦,不要把电脑搞死机啦)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class B; class A { public : shared_ptr<B> m_b; }; class B { public : shared_ptr<A> m_a; }; int main() { while ( true ) { shared_ptr<A> a( new A); //new出来的A的引用计数此时为1 shared_ptr<B> b( new B); //new出来的B的引用计数此时为1 a->m_b = b; //B的引用计数增加为2 b->m_a = a; //A的引用计数增加为2 } //b先出作用域,B的引用计数减少为1,不为0,所以堆上的B空间没有被释放,且B持有的A也没有机会被析构,A的引用计数也完全没减少 //a后出作用域,同理A的引用计数减少为1,不为0,所以堆上A的空间也没有被释放 } |
如此一来,A和B都互相指着对方吼,“放开我的引用!“,“你先发我的我就放你的!”,于是悲剧发生了。
所以在使用基于引用计数的智能指针时,要特别小心循环引用带来的内存泄漏,循环引用不只是两方的情况,只要引用链成环都会出现问题。当然循环引用本身就说明设计上可能存在一些问题,如果特殊原因不得不使用循环引用,那可以让引用链上的一方持用普通指针(或弱智能指针weak_ptr)即可。