zoukankan      html  css  js  c++  java
  • 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.


  • 相关阅读:
    unrecognized selector sent to class
    Xcode 7安装KSImageNamed 不启作用
    使用IntelliJ IDEA配置Erlang开发环境
    BN 详解和使用Tensorflow实现(参数理解)
    argparse 在深度学习中的应用
    转置卷积的详细理解
    递归该怎么写(二)
    递归该怎么写(一)
    二叉树的遍历python 代码
    经典排序的python实现
  • 原文地址:https://www.cnblogs.com/xinyuyuanm/p/3019516.html
Copyright © 2011-2022 走看看