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