Effective shared_ptr

零、前言


这篇文章本是作为: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.


 

posted @ 2013-04-13 23:23  xinyuyuanm  阅读(270)  评论(0编辑  收藏  举报