zoukankan      html  css  js  c++  java
  • C++ 智能指针 shared_ptr 分析

    引文:

    C++对指针的管理提供了两种解决问题的思路:

    1.不允许多个对象管理一个指针

    2.允许多个对象管理一个指针,但仅当管理这个指针的最后一个对象析构时才调用delete

    ps:这两种思路的共同点就是只允许delete一次,下面将讨论的shared_ptr就是采用思路1实现的

    ps:智能指针不是指针,而是类,可以实例化为一个对象,来管理裸指针


    1.shared_ptr的实现原理:

    shared_ptr最本质的功能:“当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete”,该功能是通过引用计数法实现的

    引用计数法的规则:

      1)所有管理同一个裸指针的shared_ptr,都共享一个引用计数器

      2)每当一个shared_ptr被赋值给其他shared_ptr时,这个共享的引用计数器就加1

      3)每当一个shared_ptr析构或被用于管理其他裸指针时,这个引用计数器就减1

      4)如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源

    引用计数法的内部实现:

      1)这个引用计数器保存在某个内部类型中,而这个内部类型对象在shared_ptr第一次构造时以指针的形式保存在shared_ptr中

      2)shared_ptr重载了赋值运算符,在赋值和拷贝另一个shared_ptr时,这个指针被另一个shared_ptr共享

      3)在引用计数归0时,这个内部类型指针与shared_ptr管理的资源一起释放

      4)此外,为了保证线程安全,引用计数器的加1和减1都是原子操作,它保证了shared_ptr由多个线程共享时不会爆掉


    2.shared_ptr的使用

    #include<iostream>
    #include<stdio.h>
    #include<string>
    #include<memory>
    using namespace std;
    
    int main()
    {
        //初始化 方法1:
        shared_ptr<string> sptr1(new string("name"));
        //初始化 方法2:
        shared_ptr<string> sptr2=make_shared<string>("sex");
        //初始化 方法3:
        int *p =new int(10);
        shared_ptr<int> sptr3(p); //这种初始化的方式很危险,delete p之后,strp3也不再有效
    }

    相关成员函数:

    1)use_count:返回引用计数的个数

    2)unique:返回是否独占所有权(use_count=1)

    3)swap:交换两个share_ptr对象(即交换所拥有的对象)

    4)reset:放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少

    5)get:返回内部对象指针


    3.引用计数最大的缺点:循环引用

    下面是事故现场:

    class Observer; // 前向声明
    class Subject
    {
    private:
    
        std::vector<shared_ptr<Observer>> observers;
    public:
        Subject() {}
        addObserver(shared_ptr<Observer> ob)
        {
            observers.push_back(ob);
        }
        // 其它代码
    };
    
    class Observer
    {
    private:
        shared_ptr<Subject> object;
    public:
        Observer(shared_ptr<Object> obj) : object(obj) {}
        
        // 其它代码
    };

    目标类subject连接这多个观察者类,当某个事件发生时,目标类可以遍历观察者数组observers,对观察者进行通知,而观察者类中也保留着目标类的shared_ptr,这样多个观察者之间可以以目标类为桥梁进行沟通,除了会发生内存泄漏外,这还是一种很不错的设计模式嘛……

    这里产生内存泄漏的原因就是循环引用,循环引用指的是一个引用通过一系列的引用链,竟然引回到自身,在上面的例子中,subject->observer->subject就是这么一条环形引用链,假设我们程序中只有一个变量shared_ptr<sbuject> p,此时p指向的对象不仅通过shared_ptr引向自己,还通过它包含的observer中的object成员变量引回自己,于是它的引用计数是2,每个observer的引用计数都是1,当p析构时,它的引用计数2-1=1,大于0,其析构函数不会被调用,于是p和它包含的每个observer对象在程序结束时依然驻留在内存中,没有被delete,从而造成了内存泄漏


    4.采用weak_ptr(弱引用)解决循环引用的问题:

    标准库提供了std::weak_ptr,weak_ptr是shared_ptr的观察者,它与一个shared_ptr绑定,但是却不参与引用计数的计算,在需要时,它还能生成一个与它所观察的shared_ptr共享引用计数器的新的shared_ptr,总而言之,weak_ptr的作用就是:在需要时生成一个与绑定的shared_ptr共享引用计数器的新shared_ptr,在其他时候不干扰绑定的shared_ptr的引用计数

    weak_ptr相关成员函数:

    1)lock:获得一个和绑定的shared_ptr共享引用计数器的新的shared_ptr

    2)expired:功能等价于判断use_count是否等于0,但是速度更快

    继续引用上面subject和observer的例子,来解决循环引用的问题:

    将上述例子中,observer中object成员的类型换成weak_ptr<subject>即可解决内存泄漏的问题,因为之前的observer中object成员的subject参与了引用计数,替换成weak_ptr<subject>之后没有参与引用计数,这样以来,p指向对象的引用计数为1,所以在p析构时,subject指针将被delete,其中包含的observer数组在析构时,内部的observer对象的引用计数也为0,所以他们也被deleete了,不存在内存泄漏的问题了

    class Observer; // 前向声明
    class Subject
    {
    private:
    
        std::vector<shared_ptr<Observer>> observers;
    public:
        Subject() {}
        addObserver(shared_ptr<Observer> ob)
        {
            observers.push_back(ob);
        }
        // 其它代码
    };
    
    class Observer
    {
    private:
        shared_ptr< weak_ptr<Subject> > object;
    public:
        Observer(shared_ptr<Object> obj) : object(obj) {}
        
        // 其它代码
    };


    5.错误用法1:多个无关的shared_ptr管理同一个裸指针,有可能导致二次析构

    int main()
    {
        int *a = new int(10);
    
        shared_ptr<int> p1(a);
    
        shared_ptr<int> p2(a);
    }

    p1和p2管理同一个裸指针a,此时的p1和p2有着完全独立的两个引用计数器,所以p1析构的时候会将a析构一次,p2析构的时候也会将a析构一次,C++中不允许同一个东西被析构两次,这样会导致程序爆炸

    为了避免这种情况,我们永远不要将new用在shared_ptr构造函数列表以外的地方,或者干脆不用new,改用make_shared

    另外,即使这样,也有可能导致二次析构,比如我们采用shared_ptr的get函数获得原始裸指针来构造另一个shared_ptr

    class A
    {
    public:
        std::shared_ptr<A> getShared()
        {
            return std::shared_ptr<A>(this);
        }
    };
    
    int main()
    {
        std::shared_ptr<A> pa = std::make_shared<A>();
        std::shared_ptr<A> pbad = pa->getShared();
    }

    上面的样例中,pa和pbad各自拥有一个独立的引用计数器,也有可能会导致二次析构

    总而言之:管理同一个资源的sahred_ptr,只能由同一个初始shared_ptr通过一系列赋值和拷贝构造得到,要确保其共享的是同一个引用计数器


    6.错误用法2:直接用new构造多个shared_ptr作为实参,可能会导致内存泄漏

    // 声明
    void f(A *p1, B *p2);
    
    // 使用
    f(new A, new B);

    上面的代码很容易发生内存泄漏,假如new A先发生于new B,那么如果new B抛出异常,那么new A的分配将会发生泄漏

    如果按照这种方式new多个share_ptr作为实参,依然会发生内存泄漏

    //声明
    void f(shared_ptr<A> p1,shared_ptr<B> p2);
    
    //使用
    f(shared_ptr<A> (new A),shared_ptr<B>(new B));

    因为shared_ptr的构造有可能发生在new A和new B之后,这里涉及到C++操作的sequence after性质,该性质保证:

    1)new A发生在shared_ptr<A>构造发生之前

    2)new B发生在shared_ptr<B>构造发生之前

    3)两个shared_ptr的构造发生在函数f的调用之前

    在满足上面三条性质的前提下,各操作的顺序可以任意执行

    若不使用new而是使用make_shared来构造shared_ptr,那么就不会产生内存泄漏

    //声明
    void f(shared_ptr<A> p1,shared_ptr<B> p2);
    
    //使用
    f(make_shared<A>(),make_shared<B>());

    原因很简单,依然是sequence after性质,如果两个函数的执行顺序不确定,那么当一个函数执行时,另外一个函数不会执行,于是make_shared<A>的构造完成了,即使make_shared<B>的构造抛出了异常,那么A的资源也能够被正确的释放,和上面的情形相比较,make_shared保证了第二个new发生的时候,第一个new所分配的资源已经被shared_ptr管理起来了,所以在异常发生时,能够正确的释放资源

    总结:请总是使用make_shared来生成shared_ptr


    7.如果希望使用shared_ptr来管理动态数组,那么需要提供一个自定义的删除器来代替delete

    #include <iostream>
    #include<memory>
    using namespace std;
    
    class DelTest
    {
    public:
        DelTest(){
            j= 0;
            cout<<" DelTest()"<<":"<<i++<<endl;
        }
        ~DelTest(){
            i = 0;
            cout<<"~ DelTest()"<<":"<<i++<<endl;
        }
        static int i,j;
    };
    
    int DelTest::i = 0;
    int DelTest::j = 0;
    
    void noDefine()
    {
        cout<<"no_define start running!"<<endl;
        shared_ptr<DelTest> p(new DelTest[10]);
    
    }
    
    void slefDefine()
    {
        cout<<"slefDefine start running!"<<endl;
        shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。
    }
    
    int main()
    {
        noDefine();//!构造10次,析构1次。内存泄漏。
        cout<<"--------------------"<<endl;
        slefDefine();//!构造次数==析构次数 无内存泄漏
    }
    /*
    运行结果:
    no_define start running!
     DelTest():0
     DelTest():1
     DelTest():2
     DelTest():3
     DelTest():4
     DelTest():5
     DelTest():6
     DelTest():7
     DelTest():8
     DelTest():9
    ~ DelTest():0
    --------------------
    slefDefine start running!
     DelTest():1
     DelTest():2
     DelTest():3
     DelTest():4
     DelTest():5
     DelTest():6
     DelTest():7
     DelTest():8
     DelTest():9
     DelTest():10
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    */

    需要注意的是:虽然通过自定义删除器的方式shared_ptr可以管理动态数组,但是shared_ptr并不支持下标运算符的操作,而且只能指针类型不支持指针算术运算(不能取地址),因此为了访问数组中的元素,必须用get获得一个原始内置裸指针,然后用它来访问数组元素

    样例如下:

    #include <iostream>
    #include<memory>
    using namespace std;
    
    class DelTest
    {
    public:
        DelTest(){
            j= 0;
            x=i;
            cout<<" DelTest()"<<":"<<i++<<endl;
        }
        ~DelTest(){
            i = 0;
            cout<<"~ DelTest()"<<":"<<i++<<endl;
        }
        static int i,j;
        int x;
    };
    
    int DelTest::i = 0;
    int DelTest::j = 0;
    
    void noDefine()
    {
        cout<<"no_define start running!"<<endl;
        shared_ptr<DelTest> p(new DelTest[10]);
    
    }
    
    void slefDefine()
    {
        cout<<"slefDefine start running!"<<endl;
        shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。
        cout<<p.get()[4].x<<endl;
    
    }
    
    int main()
    {
        noDefine();//!构造10次,析构1次。内存泄漏。
        cout<<"--------------------"<<endl;
        slefDefine();//!构造次数==析构次数 无内存泄漏
    }
    /*
    运行结果:
    no_define start running!
     DelTest():0
     DelTest():1
     DelTest():2
     DelTest():3
     DelTest():4
     DelTest():5
     DelTest():6
     DelTest():7
     DelTest():8
     DelTest():9
    ~ DelTest():0
    --------------------
    slefDefine start running!
     DelTest():1
     DelTest():2
     DelTest():3
     DelTest():4
     DelTest():5
     DelTest():6
     DelTest():7
     DelTest():8
     DelTest():9
     DelTest():10
    5
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    ~ DelTest():0
    */


    8.使用shared_ptr管理非常规的动态对象的时候,记得自定义删除器

    某些情况下,有些动态内存也不是我们new出来的,如果要使用shared_ptr管理这种动态内存,也要自定义删除器

    #include <iostream>
    #include <stdio.h>
    #include <memory>
    using namespace std;
    
    void closePf(FILE * pf)//即可以避免异常发生后无法释放内存的问题,也避免了很多人忘记执行fclose
    {
        cout<<"----close pf after works!----"<<endl;
        fclose(pf);
    }
    
    int main()
    {
        shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf);
        cout<<"*****start working****"<<endl;
        if(!pf)
            return -1;
        char *buf = "abcdefg";
        fwrite(buf,8,1,pf.get());//确保fwrite不会删除指针的情况下,可以将shared_ptr内置指针取出
        cout<<"------write in file!-----"<<endl;
    }
    /*
    *****start working****
    ------write in file!-----
    ----close pf after works!----
    */

    类比TCP/IP中连接打开和关闭的情况,同理都可以使用shared_ptr来管理


    总结:

    1)不用使用相同的内置/原始/裸指针初始化多个智能指针

    2)不要delete get函数返回的指针

    3)如果你使用了get返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了

    4)如果你使用的智能指针管理的资源不是new分配的内存,记得传递一个删除器

    5)请勿使用new构造多个shared_ptr作为实参,应该使用make_shared

    6)存在循环引用关系时,请使用weak_ptr来保证不会产生内存泄漏


  • 相关阅读:
    MTG Hole
    串行SPI口Flash全部引脚
    名词解释:100BASEX
    锂离子电池开路电压与电池剩余电量的对应关系
    BT.656接口数据帧的结构
    Oracle SCN是什么
    oracle联机文档
    ORACLE SERVER 组成
    struts文件上传时异常问题
    C++ NULL的使用
  • 原文地址:https://www.cnblogs.com/yinbiao/p/11563520.html
Copyright © 2011-2022 走看看