• C++智能指针


    一、为什么使用智能指针?智能指针的原理?

    C/C++中的堆内存分配和释放的方式主要是: malloc/free 以及 new/delete 等。

    使用new 和delete 管理内存存在三个常见问题:

    1.忘记delete(释放) 内存,或者异常导致程序过早退出,没有执行 delete。忘记释放动态内存会导致内存泄露问题,长时间这样会导致系统内存越来越小。 (内存泄露问题往往很难查找到,内存耗尽时,才能检测出这种错误)

    2.使用已经释放掉的对象。比如:我们使用delete释放掉申请的内存空间,但并未去除指向这片空间的指针,此时指针指向的就是“垃圾”内存。

    3.同一块内存释放两次。当有两个指针指向相同的动态内存分配对象时,其中一个进行了delete操作,对象内存就还给了操作系统 ,如果我们要delete第二个指针,那么内存有可能遭到破坏。(浅拷贝问题)

    使用智能指针可以很大程度上的避免这些问题。

    智能指针就是一个类,类的构造函数中传入一个普通指针,当超出了类的作用域时,类会自动调用析构函数,释放资源。其核心思想是:栈上对象在离开作用范围时会自动析构。

    智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr,auto_ptr在C++11被弃用。

    二、常用的智能指针

    1、auto_ptr:自动指针,自动回收。在构造对象时赋予其管理空间的所有权,在拷贝或赋值中转移空间的所有权,拷贝和赋值后直接将_ptr赋为空,禁止其再次访问原来的内存空间。

    构造函数:explicit关键字修饰的构造函数不能被隐式调用,只能显示调用

    explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()   
            : _Myptr(_Ptr)     // 将指针交由auto_ptr托管
    {   // construct from object pointer
    }

    析构函数:

    // 释放了托管的对象所占用的内存空间
    ~auto_ptr()
    {  
        delete _Myptr;
    }

    get方法:

    // 返回保存的指针
    _Ty *get() const _THROW0()
    {   
        return (_Myptr);
    }

    release方法:

    _Ty *release() _THROW0()
    {    // return wrapped pointer and give up ownership  返回保存的指针,对象中不保留原来的指针,原来的指针直接赋值为0
        _Ty *_Tmp = _Myptr;
        _Myptr = 0;
        return (_Tmp);
    }

    reset方法:

    void reset(_Ty* _Ptr = 0)
    {   // 重置auto_ptr使之拥有另一个对象。先删除已经拥有的对象,然后新建一个并拥有一个新对象
        if (_Ptr != _Myptr)
            delete _Myptr;
        _Myptr = _Ptr;
    }

    拷贝构造函数:

    // 明显可看出会发生托管权的转移
    auto_ptr(auto_ptr<_Ty>& _Right) _THROW0()
            : _Myptr(_Right.release())
    {    // construct by assuming pointer from _Right auto_ptr
    }

    赋值运算符:

    // 很明显也发生了托管权的转移
    template<class _Other> auto_ptr<_Ty>& operator=(auto_ptr<_Other>& _Right) _THROW0()
    {   // assign compatible _Right (assume pointer)
        reset(_Right.release());
        return (*this);
    }

    使用:

    void fun()
    {
       T *pt = new T();
    
       // 将分配的堆内存指针交由auto_ptr托管
       std::auto_ptr apt(pt);      //显式调用构造函数
       
       // 像正常使用指针一样使用,相当于*pt= 10
       *apt = 10;
       
       // 相当于 pt->memFunc()
       apt->memFunc(); 
       
       // 使用get函数可获取它托管的指针
       T *pt2 = apt.get(); 
    
       // 可调用reset函数更改托管对象,这里删除了之前托管的 pt
       apt.reset(new T()); 
    
       // 可调用release函数放弃托管
       T *pt3 = apt.release(); 
    
       // 放弃托管意味着又需要自己手动释放内存了
       delete pt3;
       pt3 = NULL;
      
       return;
    }

    注意:

    (1) auto_ptr没有使用引用计数,如果多个auto_ptr指向同一个对象,就会造成对象被删除一次以上的错误。因此一个对象只能由一个auto_ptr所拥有,在给其他auto_ptr赋值的时候,会转移这种拥有关系。在赋值、参数传递的时候会转移所有权,不要轻易进行此类操作。

    /* 1. 演示转移所有权 */
    std::auto_ptr<int> aptr1(new int(3));  
    // 执行后aptr1不再有效
    std::auto_ptr<int> aptr2 = aptr1;  // or aptr2(aptr1)     
    // 强行访问会发生不可预料的问题
    *aptr1 = 4;    
    // --------------------------------------------------------
    /* 2. 演示参数传递的所有权转移 */ void lose(std::auto_ptr<int> a) {  // 空函数,仅仅为了演示参数传递 } std::auto_ptr<int> aptr3(new int(4)); // 所有权转移,aptr3不再有效 lose(aptr3); // 强行访问会发生不可预料的问题 *aptr3 = 10;

    (2) auto_ptr的析构函数内部释放资源时调用的是delete而不是delete[],因此不要让auto_ptr托管数组。

    (3) auto_ptr不能作为容器对象,因为容器中的元素经常要进行拷贝,赋值等操作,在这过程中auto_ptr会失去所有权。

    (4) 判断auto_ptr是否为空不能使用if(aptr == NULL),应该使用if(aptr.get() == NULL)

    2、unique_ptr:是指”唯一”地拥有其所指对象的智能指针,同一时刻只能有一个unique_ptr指向给定对象(使用移动语义来实现),与auto_ptr的不同点

    (1) 可以通过间接的方式用于容器中

    unique_ptr<int> sp(new int(10));
    vector<unique_ptr<int> > vec;
    vec.push_back(std::move(sp));    //通过这种移动语义来实现在容器中使用
    
    vec.push_back(sp);               //这样直接使用不行,会报错
    cout << *sp << end;              //这样也不行,因为sp添加到容器中后,会报错

    (2) 无法直接进行复制构造与赋值操作,要使用move函数进行所有权的转移

    unique_ptr<int> uq(new int(10));
    unique_ptr<int> uq2 = uq;             //会报错,auto_ptr中可以
    unique_ptr<int> uq3(uq);              //同样会报错,auto_ptr中可以
    unique_ptr<int> uq4 = std::move(up); //使用move函数 直接显式的所有权转移是可以的

    (3) 可以用于函数的返回值

    // 函数定义
    unique_ptr<int> myFunc()
    {
        unique_ptr<int> up(new int(10));
        return up;
    }
    
    unique_ptr<int> upRet = myFunc();

    (4) 可以直接用if(ptr == NULL)来判断是否空指针

    使用:

    unique_ptr<int> up(new int(3));       //托管一个对象
    
    // 更改所有权
    unique_ptr<int> up2 = std::move(up);  //所有权转移,转移后,up变为空指针
    int *p = up.release();                //释放所有权
    up.reset();                           //显式销毁所有权

    3、shared_ptr:使用计数机制来表明资源被几个指针共享。与auto_ptr的不同点

    (1) 使用一个引用计数shared_count,用来表示当前有多少个智能指针对象共享指针指向的内存块,可以通过成员函数use_count()来查看资源的所有者个数。

    (2) 析构函数中对引用计数进行判断,如果 shared_count > 1,则不释放内存只是将引用计数减1,当shared_count == 1的时候释放内存。当调用release()时,当前指针会释放资源所有权,计数减1,当计数等于0时,资源会被释放。

    (3) 复制构造与赋值操作符除了提供复制功能之外,还将引用计数加1。

     使用:

    // 1. 构造方法
    // 将指针交由shared_ptr托管  还有一种方式也可以创建shared_ptr对象,且比较常用,通过make_shared函数: shared_ptr<int> shPtr = make_shared<int>(10); 
    shared_ptr<int> shPtr(new int(10)); 
    int num = *shPtr;   // 像使用正常指针一样使用它,此时num == 10
    
    // 2. 复制构造函数
    shared_ptr<int> shPtr2(shPtr);    // 复制构造,此时引用计数会增加
    // 两个shared_ptr相等,指向同一个对象,引用计数为2
    assert(shPtr == shPtr2 && shPtr.use_count() == 2); 
    // 原先的shPtr还可以继续使用,如果是auto_ptr,是不能使用的,因为有所有权的转移
    num = *shPtr;      
    *shPtr = 20;    
    assert(*shPtr2 == 20);  // 在改一个shared_ptr的同时,另一个也会更改
    
    // 3. 赋值运算符
    shared_ptr<int> shPtr3 = shPtr2;  // 赋值操作符
    
    // 4. 停止使用
    shPtr.reset();
    assert(!shPtr);   // shPtr停止使用后会变成空指针

    注意:

    (1) shared_ptr不能对循环引用的对象的内存进行自动管理,循环引用会导致堆内存无法正确释放,内存泄漏。循环引用在weak_ptr中介绍。

    (2) 不要构造一个临时的shared_ptr作为函数的参数,存在内存泄漏的风险

    void  f(shared_ptr<int>, int);
    int g();
    // 正确的使用方式
    void OK()
    {
        shared_ptr<int> p(new int(2));
        f(p, g());
    }
    
    // 错误的使用方式
    void Bad()
    {
        // 如果执行顺序是先new int(2), 然后g(), 最后将 new int(2)的指针给shared_ptr的构造函数的话,当g()中抛出异常的时候,第一个new int(2)就造成了内存泄漏了
        f(shared_ptr<int>(new int(2)), g());
    }

    4、weak_ptr:相对于shared_ptr这种强引用类型的智能指针, weak_ptr是一种弱引用型的指针。用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。可以看成是shared_ptr的助手而不是真正的智能指针,因为它不会托管资源,它的构造也不会引起引用计数的增加,且没有重载 operator * 和 operator ->,不具有普通指针的行为。和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

     循环引用:

    class B;
    class A
    {
    public:
        A()
        {
            cout << "Class A Constructor is called." << endl;
        }
        ~A()
        {
            cout << "Class A Deconstructor is called." << endl;
        }
    
        //tr1::shared_ptr<B> m_shB;
        tr1::weak_ptr<B> m_shB;
    };
    
    class B
    {
    public:
        B()
        {
            cout << "Class B Constructor is called." << endl;
        }
        ~B()
        {
            cout << "Class B Deconstructor is called." << endl;
        }
    
        tr1::shared_ptr<A> m_shA;
    };
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        {
            // 测试重复引用
            tr1::shared_ptr<A> shA(new A());   //shA引用计数:1
            tr1::shared_ptr<B> shB(new B());   //shB引用计数:1
            
            if (shA && shB)
            {
                shA->m_shB = shB;   //因为m_shB是weak_ptr对象,不会引起计数增加,shB引用计数仍为1。若m_shB是shared_ptr对象,则shB引用计数变为2.
                shB->m_shA = shA;   //shA引用计数变为2
            }
    
            cout << "A的引用计数:" << shA.use_count() << " B的引用计数:" << shB.use_count() << endl;
            cout << "要离开shA和shB的作用域了,正常情况下在这之后会执行shA和shB的析构函数的" << endl;
                
            // 这里是要执行析构函数的
            // 首先,会执行shB这个B对象的析构函数,要析构B的话,得先去判断下托管B的shared_ptr的引用计数,(若这里是2,则不能析构B,B的成员对象A自然也不能析构,从而死锁)
            //       这里是1,所以去析构B,B析构后紧接着去析构其成员对象A,此时A的引用计数为2,所以会使A的引用计数减为1
            // 然后,会执行shA这个A对象的析构函数,要析构A的话,也得先去判断下托管的A的shared_ptr的引用计数,这里是1,它可以析构
        }
        cout << "已经离开shA和shB的作用域了,请观察shA和shB的析构函数有没有被执行" << endl;
    }

    参考:https://jocent.me/2017/05/31/cpp_smart_pointer.html

  • 相关阅读:
    仿新浪微博返回顶部的js实现(jQuery)
    PHP中实现页面跳转
    WPF 导出数据
    1.C#泛型-泛型集合Dictionary<Key,Value>
    把重载的那些消息都看看,熟悉一下功能
    mfc的 windows消息处理
    文本框控件字体,背景色都可以单独设置。
    体验了一下msdn2012,挺好用的,可以找到所有的函数,进行调用,还有例子。
    WPF
    VC控件ListCtrl的使用方法总汇
  • 原文地址:https://www.cnblogs.com/yapp/p/10133327.html
Copyright © 2020-2023  润新知