C++智能指针

1. 智能指针

所谓智能指针其实是一些模板类,它们负责自动管理一个指针的内存,免去了手动 new/delete 的麻烦,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。

智能指针利用了RAII(资源获取即初始化)的技术,对普通指针进行封装,使得其行为像一个指针,但其实是一个对象。把资源放进对象内,用资源来管理对象,便是 C++ 编程中最重要的编程技法之一,即 RAII ,它是 "Resource Acquisition Is Initialization" 的首字母缩写。

智能指针的作用:方便堆内存管理

  • 忘记delete 内存:会导致内存泄漏问题,且除非是内存耗尽否则很难检测到这种错误。
  • 使用已经释放掉的对象:如果能够记得在释放掉内存后将指针置空并在下次使用前判空,尚可避免这种错误。
  • 同一块内存释放两次:如果有两个指针指向相同的动态分配对象,则很容易发生这种错误。
  • 发生异常时的内存泄漏:若在new 和 delete 之间发生异常,则会导致内存泄漏。

野指针(wild pointer)就是没有被初始化过的指针

在这里插入图片描述

2. 三种智能指针

C++11中所新增的智能指针包括shared_ptrunique_ptrweak_ptr,在C++11之前还存在auto_ptr(C++17废弃)。三种类型都定义在头文件memory中。auto_ptr可能导致对同一块堆空间进行多次delete,即当两个智能指针都指向同一个堆空间时,每个智能指针都会delete一下这个堆空间,这会导致未定义行为。

让所有的智能指针都有名字

智能指针为解决资源泄漏、编写异常安全代码提供了一种解决方案,那么他是万能的良药吗?使用智能指针,就不会再有资源泄漏了吗?请看下面的代码:

上面的函数调用,看起来是安全的,但在现实世界中,其实不然:由于C++并未定义一个表达式的求值顺序,因此上述函数调用除了func在最后得到调用之外是可以确定,其他的执行序列则很可能被拆分成如下步骤:

  • 1、分配内存给T1
  • 2、分配内存给T2
  • 3、构造T1对象
  • 4、构造T2对象
  • 5、构造T1的智能指针对象
  • 6、构造T2的智能指针对象
  • 7、调用func

此时,如果程序在第3步失败,那T1和T2对象所分配内存必然泄漏。而解决这个问题的方案也很简单,就是不要在函数实参中创建shared_ptr,抛弃临时对象,让所有的智能指针都有名字,就可以避免此类问题的发生。比如以下代码:

优先选用make_shared/make_unique而非直接使用new。简单说来,相比于直接使用new表达式,make系列函数有三个优点消除了重复代码、改进了异常安全性和生成的目标代码尺寸更小速度更快

shared_ptr

  • shared_ptr多个指针指向相同的对象,使用引用计数来完成自动析构的功能。
  • shared_ptr的引用计数是线程安全的,但是其对象的写操作在多线程环境下需要加锁实现。
  • 不要用同一个指针初始化多个shared_ptr,这样可能会造成二次释放
std::unique_ptr<int> ptr0 = std::make_unique<int>(); //C++14 以及之后才可以
std::shared_ptr<int> ptr1 = std::make_shared<int>(); //C++11可以
 //or
std::unique_ptr<int> ptr0(new int);
std::unique_ptr<int> ptr1(new int);

尽量使用第一种方法,这也是C++官方推荐的。


#include<iostream>
#include<memory>

using namespace std;

int main() {
    int a = 10;
    shared_ptr<int> ptra = make_shared<int>(a);
    shared_ptr<int> ptra2(ptra);     //拷贝构造函数
    cout << ptra.use_count() << endl;   //2

    int b = 20;
    int *pb = &b;

    shared_ptr<int> ptrb = make_shared<int>(b);
    ptra2 = ptrb;
    pb = ptrb.get();    //获取指针

    cout << ptra.use_count() << endl;   //1
    cout << ptrb.use_count() << endl;   //2

    return 0;
}

shared_ptr 的问题

  • shared_ptr的尺寸是裸指针的两倍:因为内部既包含一个指向该资源的裸指针,也包含一个指向该资源的引用计数的裸指针。
  • 引用计数的内存必须动态分配
  • 引用计数的递增和递减必须是原子操作:原子操作一般比非原子操作慢。我们的实现版本里为了简单起见没有实现原子操作。
  • shared_ptr 的循环引用问题:shared_ptr 意味着你的引用和原对象是一个强联系。你的引用不解开,原对象就不能销毁。滥用强联系,这在一个运行时间长、规模比较大,或者是资源较为紧缺的系统中,极易造成隐性的内存泄漏,这会成为一个灾难性的问题。更糟的是,滥用强联系可能造成循环引用的灾难。即:B持有指向A内成员的一个shared_ptr,A也持有指向B内成员的一个 shared_ptr,此时A和B的生命周期互相由对方决定,事实上都无法从内存中销毁。 更进一步,循环引用不只是两方的情况,只要引用链成环都会出现问题。weak_ptr 的用处 就是用来辅助解决循环引用

weak_ptr

  • weak_ptr是为了配合shared_ptr而引入的一种智能指针,它不具备普通指针的行为,没有重载operator*和->。其最大的作用是协助shared_ptr工作,像旁观者那样检测资源的使用情况,解决shared_ptr相互引用时的死锁问题。
  • weak_ptr可以从一个shared_ptr或另一个weak_ptr对象构造,获得资源的观测权。
  • weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数。
  • 另一个成员函数expired()等价于判断use_count()==0
  • weak_ptr和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
#if 1
#include<iostream>
#include<memory>
using namespace std;
class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
    {
        cout<<"A delete."<<endl;
    }


};

class B 
{
    public:
        shared_ptr<A> pa_;
        ~B()
        {
            cout<<"B delete."<<endl;
        }
};

void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}

int main()
{
    fun();
    return 0;

}

#endif

 运行结果

分析
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),
如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr<B> pb_; 改为weak_ptr<B> pb_;

     

这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

shared_ptr的实现

shared_ptr 必须管理一个计数器指针,在初始化和删除时需要修改这个指针的内容。所以它必须有2个成员对象:计数器的指针和数据指针: T* (that is returned by operator-> and dereferenced in operator*) and a aux* where aux is a inner abstract class that contains:

  • a counter (incremented / decremented upon copy-assign / destroy)
  • whatever is needed to make increment / decrement atomic (not needed if specific platform atomic INC/DEC is available)
  • an abstract virtual destroy()=0;
  • a virtual destructor.

所以通过shared_ptr 初始化一个变量,它所占的内存为机器内存地址的2倍,64位的系统为16bit。

Such aux class (the actual name depends on the implementation) is derived by a family of templatized classes (parametrized on the type given by the explicit constructor, say U derived from T), that add:

  • a pointer to the object (same as T*, but with the actual type: this is needed to properly manage all the cases of T being a base for whatever U having multiple T in the derivation hierarchy)
  • a copy of the deletor object given as deletion policy to the explicit constructor (or the default deletor just doing delete p, where p is the U* above)
  • the override of the destroy method, calling the deleter functor.

一个骨架实现:

template<class T>
class shared_ptr
{
    struct aux
    {
        unsigned count;

        aux() :count(1) {}
        virtual void destroy()=0;
        virtual ~aux() {} //must be polymorphic
    };

    template<class U, class Deleter>
    struct auximpl: public aux
    {
        U* p;
        Deleter d;

        auximpl(U* pu, Deleter x) :p(pu), d(x) {}
        virtual void destroy() { d(p); } 
    };

    template<class U>
    struct default_deleter
    {
        void operator()(U* p) const { delete p; }
    };

    aux* pa;
    T* pt;

    void inc() { if(pa) interlocked_inc(pa->count); }

    void dec() 
    { 
        if(pa && !interlocked_dec(pa->count)) 
        {  pa->destroy(); delete pa; }
    }

public:

    shared_ptr() :pa(), pt() {}

    template<class U, class Deleter>
    shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}

    template<class U>
    explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}

    shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }

    template<class U>
    shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }

    ~shared_ptr() { dec(); }

    shared_ptr& operator=(const shared_ptr& s)
    {
        if(this!=&s)
        {
            dec();
            pa = s.pa; pt=s.pt;
            inc();
        }        
        return *this;
    }

    T* operator->() const { return pt; }
    T& operator*() const { return *pt; }
};

Where weak_ptr interoperability is required a second counter (weak_count) is required in aux (will be incremented / decremented by weak_ptr), and delete pa must happen only when both the counters reach zero.

 unique_ptr

  • unique_ptr 唯一拥有所指对象,同一时刻只能有一个unique_ptr指向给定对象,这是通过禁止拷贝语义、只允许移动语义来实现的。
  • unique_ptr指针本身的生命周期是从创建开始,直到离开作用域。在智能指针生命周期内,可以改变指针所指向对象,如创建智能指针时使用构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
  • 只有一个智能指针能真正指向一个特定的对象,也只有该指针能析构这个对象所占用的空间,直到把这个指针赋给另一个指针,后一个指针才能真正指向这个对象,而前一个指针就不再起作用了,从而避免了两次delete而导致的未定义行为。

#include<iostream>
#include<memory>

using namespace std;

int main() {
    {
        unique_ptr<int> uptr(new int(10));  //绑定动态对象

        unique_ptr<int> uptr2 = uptr;   //不能赋值
        unique_ptr<int> uptr2(uptr);    //不能拷贝

        unique_ptr<int> uptr2 = std::move(uptr);    //转换所有权
        uptr2.release();    //释放所有权
    }
    
    //超出uptr作用域,内存释放
}

3. 智能指针的实现

总体来说,实现智能指针的主要任务在于实现引用计数的维护。

首先不应该直接在智能指针类中维护引用计数,这是因为如果有多个智能指针对象指向同一对象,若改变引用计数的值,需要找到这个对象的所有智能指针对象,从而改变所有的引用计数。

所以,有两个思路可以考虑:一个是使用辅助类来维护引用计数,另一个构造句柄类

其中,辅助类采用继承的方式实现,这种方式的缺点是实现比较复杂,需要所有使用智能指针的对象都先继承自辅助类。

句柄类的方式,是在智能指针类中维护一个指向引用计数的指针,这个引用计数是一个单独的内存空间中。

3.1 使用辅助类维护引用计数

定义一个具体类(U_Ptr)来封装引用计数和指针。在创建智能指针类之前,这个类的所有成员皆为私有类型,因为它不被普通用户所使用。为了只为智能指针使用,还需要把智能指针类声明为该类的友元。类中包含含两个数据成员:计数count与之前HasPtr类中的指针( int *ptr;)。图示:
在这里插入图片描述

#ifndef _UPtr_H
#define _UPtr_H
#include <iostream>
using namespace std;

template <typename T>
class HasPtr;

template <typename T>
class U_Ptr {
    private:
    friend class HasPtr<T>;

    U_Ptr(T *p);
    ~U_Ptr();
    
    T *m_ip;
    int m_useCount;
};

template <typename T> 
U_Ptr<T>::U_Ptr(T *p)
    :m_ip(p)
    ,m_useCount(1)
{

}

template <typename T> 
U_Ptr<T>::~U_Ptr()
{
    cout<<"U_Ptr destruct"<<endl;
    if (NULL != m_ip) {
    	delete m_ip;
    	m_ip = NULL;
    }
}
#endif

#ifndef _HasPtr_H
#define _HasPtr_H
#include "uPtr.h"
#include <iostream>
using namespace std;
template <class T>
class HasPtr {

    public:
        HasPtr(T *p, int i);
        HasPtr(const HasPtr &ptr);
        HasPtr& operator=(const HasPtr &rhs);
        ~HasPtr();
        T *get_ptr() const;
        void set_ptr(T *p);

        int get_int() const;
        void set_int(int i);

        T get_ptr_val() const;
        void set_ptr_val(T val);

    private:
        U_Ptr<T> *m_uptr;
        int m_val;

};
template <typename T>
HasPtr<T>::HasPtr(T *p, int i)
    :m_uptr(new U_Ptr<T>(p))
    ,m_val(i)
{
    cout<<"HasPtr constructor"<<endl;
}

template <typename T>
HasPtr<T>::HasPtr(const HasPtr &ptr)
    :m_uptr(ptr.m_uptr)
    ,m_val(ptr.m_val)
{
    ++m_uptr->m_useCount;
    cout<<"HasPtr copy constructor m_uptr->m_useCount = "<<m_uptr->m_useCount<<endl;
}

template <typename T>
HasPtr<T>& HasPtr<T>::operator=(const HasPtr &rhs)
{
    bool uptrIsSame = this->m_uptr == rhs.m_uptr;
    cout<<"HasPtr assignment rhs.m_uptr->m_useCount = "<<rhs.m_uptr->m_useCount<<endl;
    cout<<"HasPtr assignment m_uptr->m_useCount = "<<m_uptr->m_useCount<<endl;
    if (!uptrIsSame) {
        ++rhs.m_uptr->m_useCount;
        if (--m_uptr->m_useCount == 0) {
            delete m_uptr;
            m_uptr = NULL;
        }
        m_uptr = rhs.m_uptr;
        m_val = rhs.m_val;
    }
    return *this;
}

template <typename T>
T *HasPtr<T>::get_ptr() const {
    return m_uptr->m_ip;
}

template <typename T>
void HasPtr<T>::set_ptr(T *p) {
    m_uptr->m_ip = p;
}

template <typename T>
int HasPtr<T>::get_int() const {
    return m_val;
}

template <typename T>
void HasPtr<T>::set_int(int i) {
    m_val = i;
}

template <typename T>
T HasPtr<T>::get_ptr_val() const {
    return *m_uptr->m_ip;
}

template <typename T>
void HasPtr<T>::set_ptr_val(T val) {
    *m_uptr->m_ip = val;
}

template <typename T>
HasPtr<T>::~HasPtr() {
    cout<<"HasPtr destruct m_uptr->m_useCount = "<<m_uptr->m_useCount<<endl;
    if (--m_uptr->m_useCount == 0) {
        delete m_uptr;
        m_uptr = NULL;
    }
}
#endif

int main() {
    int *p = new int(12);
    HasPtr<int> ptr(p, 20);
    HasPtr<int> ptr1(ptr);
    HasPtr<int> ptr2(ptr1);
    {
        HasPtr<int> ptr3 = ptr1;
        ptr3 = ptr;
    }
    return 0;
}

3.2 智能指针类中维护引用计数(句柄类)

#include <iostream>
#include <memory>

template<typename T>
class SmartPointer {
private:
    T* _ptr;
    size_t* _count;
public:
    SmartPointer(T* ptr = nullptr) :
            _ptr(ptr) {
        if (_ptr) { //需要判读,否则引用一个空指针会有问题
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    SmartPointer(const SmartPointer& ptr) {
        if (this != &ptr) {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count)++;
        }
    }

    SmartPointer& operator=(const SmartPointer& ptr) {
        if (this->_ptr == ptr._ptr) {
            return *this;
        }

        if (this->_ptr) {
            (*this->_count)--;
            if (this->_count == 0) {
                delete this->_ptr;
                delete this->_count;
            }
        }

        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count)++;
        return *this;
    }

    T& operator*() {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);

    }

    T* operator->() {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    ~SmartPointer() {
        (*this->_count)--;
        if (*this->_count == 0) {
            delete this->_ptr;
            delete this->_count;
        }
    }

    size_t use_count(){
        return *this->_count;
    }
};

int main() {
    {
        SmartPointer<int> sp(new int(10));
        SmartPointer<int> sp2(sp);
        SmartPointer<int> sp3(new int(20));
        sp2 = sp3;
        std::cout << sp.use_count() << std::endl;
        std::cout << sp3.use_count() << std::endl;
    }
    //delete operator
}

4. 实战

  • 使用 std::tr1::enable_shared_from_this 作为基类。比如:
class A : public std::tr1::enable_shared_from_this<A>
{
public:
    std::tr1::shared_ptr<A> getSharedPtr() {
    return shared_from_this();
  }
};

当使用了 shared_ptr 的时候,我们可能需要在所有的地方都使用它,否则就不容易达到管理生存期的目的了。但有的时候,我们手头上只有对象的原始指针,比如在对象的函数内部,我们只有 this。这就迫切的需要一个功能:如何从对象的裸指针中,生成我们需要的 shared_ptr。

有人可能会觉得这个简单,shared_ptr a(this); 不就行了么?很遗憾的告诉你,这样不行,会出问题。为什么呢?因为这里的 a,手中对 this 的引用计数只有 1,它无法知道其他地方智能指针对 this 这个指针(就是这个对象)的引用情况,因此当 a 的生命周期结束(比如函数返回)的时候,this 就会被它毫不留情的释放掉,其他地方的相关智能指针,手中拿着的该对象指针已经变成非法。因此,我们需要使用std::tr1::enable_shared_from_this 作为基类将类的this指针的引用导出来.

 5. 参考


版权声明:本文为liqingbing12原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>