零、前言
这篇文章本是作为:C++ 智能指针类的第二部分,但无奈那篇篇幅已经不能再长了,于是只好将其单独写成一篇,且把 shared_ptr 的循环引用放在这里写,这样稍微比较连贯一些。
一、shared_ptr 的循环引用
定义:所谓循环引用,可类比于这样的一棵树,它含有父亲结点指向孩子结点的指针,也有孩子结点指向父亲结点的指针,即父亲结点与孩子结点互相引用。
可先看一个例子(改编自:智能指针的死穴---循环引用):
#include <iostream> #include <memory> using namespace std; class B; class A { public: A(){cout<<"A constructor"<<endl;} ~A(){cout<<"A destructor"<<endl;} shared_ptr<B> m_b; }; class B { public: shared_ptr<A> m_a; B(){cout<<"B constructor"<<endl;} ~B(){cout<<"B destructor"<<endl;} }; int main() { cout<<"shared_ptr cycle reference:\n"; shared_ptr<A> a(new A); shared_ptr<B> b(new B); a->m_b = b; //cycle reference b->m_a = a; return 0; }
输出:
由输出结果可以看出:A 和 B 的析构函数都是没有执行的,内存泄露!
分析:众所周知,new 出来的对象,必须由程序员自己 delete 掉,在此运用了智能指针:shared_ptr来指向 new A,即现在 delete 的责任落到了 shared_ptr 的身上(在其退出作用域时)。但是分析下上面的代码:b 先出作用域(析构顺序与构造相反),B 的引用计数减为1,不为0,故堆上B的空间没有释放,此时的结果是:b 走了,但是 new B 并没有被 delete 掉,好吧,现在只有等待 a 来delete了。然后是 a 退出其作用域,A 的引用计数减少为1,不为0,因为B中的 m_a指向它,结果是:a 走了,但是 new A 并没有被 delete 掉,而此时已经没有 share_ptr 对象可以将他们delete掉了,不对,好像还有:存在于new 出来的A和B对象里,如果没有delete,他俩就不会超出作用域,它们在等待delete,而 delete 却在等待 shared_ptr 对象自身发出delete,矛盾产生,于是就这样死锁了!!!故 new 出 来的 A 和 B 就这样的被遗弃,从而内存泄露了。
原因:(1)new 出来的对象必须手动delete掉;(2)掌握delete的shared_ptr 在 new 出来的对象之中;(3)两个new 对象里的shared_ptr 互相等待。
解锁:试想如果只有单向指向,如上代码:去掉一行:b->m_a =a ;,但是将 B 引用 A 的信息保存在某处,且对于 A 和 B的shared_ptr 对象是不可见的,但是这些信息却可以观察到 指向 A 和 B 的 shared_ptr 对象的行为。再来分析一下:b 先出作用域,B的引用计数减少为1,不为0,此时 堆上 B 的空间没有释放,结果依旧:b 走了,但是 new B 并没有被 delete 掉。然后是 a 退出作用域,注意:此时 A 的引用计数减少为0,资源A 被释放,这也导致A 空间中的指向资源B shared_ptr对象超出作用域,从而 B的引用计数减少为0,释放B,如此 A 和 B 均能正确的释放了,这应该就是weak_ptr 智能指针的原型了。
再来看下原来的例子(加入了 weak_ptr):
#include <iostream> #include <memory> using namespace std; class B; class A { public: A(){cout<<"A constructor"<<endl;} ~A(){cout<<"A destructor"<<endl;} shared_ptr<B> m_b; }; class B { public: weak_ptr<A> m_a; B(){cout<<"B constructor"<<endl;} ~B(){cout<<"B destructor"<<endl;} }; int main() { cout<<"shared_ptr cycle reference:\n"; shared_ptr<A> a(new A); shared_ptr<B> b(new B); cout<<"a counter: "<<a.use_count()<<endl; cout<<"b counter: "<<b.use_count()<<endl; a->m_b = b; //cycle reference b->m_a = a; cout<<"a counter: "<<a.use_count()<<endl; cout<<"b counter: "<<b.use_count()<<endl; cout<<"b->m_a counter: "<<b->m_a.use_count()<<endl; //that is the reference counts of A cout<<"expired: "<<std::boolalpha<<b->m_a.expired()<<endl; return 0; }
输出:
可见:此时 A 和 B 都成功地析构了。
二、shared_ptr 的重复析构
在shared_ptr 中看到【重复析构】这个词,其实有点诧异,因为 share_ptr 不正是由于普通指针(raw pointer)可能的内存泄露和重复析构而提出的嘛,怎么自身还有重蹈覆辙呢?
原因就在于,很多时候没有完全使用 shared_ptr ,而是普通指针和智能指针混搭在一起,或是很隐蔽地出现了这样情况,都会导致重复析构的发生。
场景1---最简单地混搭:
int* pInt = new int(10); shared_ptr<int> sp1(pInt); ... shared_ptr<int>sp2(pInt);
由 shared_ptr 的构造函数以及其源码(关于 shared_ptr 源码可见:
std::tr1::shared_ptr源码 和
shared_ptr源码解读):
//constructor template<class T> explicit shared_ptr(T* ptr); ... //tr1::shared_ptr source code ... public: shared_ptr(T* p = NULL) { m_ptr = p; m_count = new sp_counter_base(1, 1); _sp_set_shared_from_this(this, m_ptr); } ...
根据 shared_ptr 的源码
可知:此时,由普通指针构造出来的shared_ptr(包括引用计数和控制块),其将新生成一个引用计数类(new sp_counter_base(1, 1)
)引用计数初始化为1。如果后面再有一个此类的构造函数(对同一个普通指针),则又会重新构造出一个 引用计数类,并且是引用计数初始化为1(而不是加1变成2)。这样就会导致后期的重复析构了。
场景2---与 this 指针的混搭
#include <iostream> #include <memory> using namespace std; class A { private: public: A(){cout<<"constructor"<<endl;} ~A(){cout<<"destructor"<<endl;} shared_ptr<A> sget() { shared_ptr<A> sp(this); cout<<"this: "<<this<<endl; return sp; } }; int main() { shared_ptr<A> test (new A); shared_ptr<A> spa = test->sget(); cout<<"spa: "<<spa<<endl; cout<<"test: "<<test<<endl; cout<<"spa counter: "<<spa.use_count()<<endl; cout<<"test counter: "<<test.use_count()<<endl; return 0; }
输出:
程序出现【core dumped】,根据程序crash之前的信息可知:
A 对象析构的两次,原因在于 sget()函数内部的 临时shared_ptr 对象 sp 是由普通指针this 构造而来,故生成的shared_ptr 对象将生成一个新的引用计数类(不同于test的),并初始化计数为1。这将导致 test 和 spa 退出各自作用域时均执行 A 的析构函数,析构两次。
解决办法:C++11中提供了 enable_from_shared_this 类,其他类可继承它,并使用 shared_from_this方法获得类对象的shared_ptr智能指针,此时使用的引用计数类一样(具体实现与weak_ptr类有关,详情可参见shared_from_this源码)。
(1)让 A继承 enable_from_shared_this 类
(2)修改 sget 函数,调用 shared_from_this方法获得类对象的shared_ptr版本
#include <iostream> #include <memory> using namespace std; class A :public enable_shared_from_this<A> { private: public: A(){cout<<"constructor"<<endl;} ~A(){cout<<"destructor"<<endl;} shared_ptr<A> sget() { return shared_from_this(); } }; int main() { shared_ptr<A> test (new A); shared_ptr<A> spa = test->sget(); cout<<"spa: "<<spa<<endl; cout<<"test: "<<test<<endl; cout<<"spa counter: "<<spa.use_count()<<endl; cout<<"test counter: "<<test.use_count()<<endl; return 0; }
输出:
此时只析构一次,且test和spa的引用计数为同一引用计数类,值均为2.