智能指针

为什么要有智能指针?原生态指针的缺陷?
使用原生态指针时我们需要时刻注意空间的申请和释放,尤其在处理异常时,我们必须在抛出异常前把程序中动态开辟的内存空间释放掉,有时会使代码显得臃肿,所以我们引入智能指针的概念,就是RAII,是一种用对象的生命周期控制程序资源的方式在对象构造期间(构造函数)获取资源,在对象销毁时(析构函数)释放资源,这样我们就不用显示的释放资源,并且对象所需的资源在对象的生命周期内始终有效。
用RAII思想实现简单的智能指针:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p=nullptr)
        :_ptr(p)
    {}
    ~SmartPtr()
    {
        if (_ptr){
            delete _ptr;
        }
    }
private:
    T* _ptr;
};

这段代码就完成了通过类管理资源的作用,但并不具有指针的特性,即虚加入解引用和->操作方式,如下:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p=nullptr)
        :_ptr(p)
    {}
    ~SmartPtr()
    {
        if (_ptr){
            delete _ptr;
        }
    }
    T &operator*()
    {
        return *_ptr;
    }
    T * operator->()
    {
        return _str;
    }
private:
    T* _ptr;
};

用这种方式我们就可以在主函数调用我们自己实现的这个智能指针: SmartPtr<int> p1(new int);就不需要再显示销毁它了(已将在析构函数中完成了空间的释放)。
这种方式虽然可以帮助我们不操心内存的释放,但执行下面的代码时:SmartPtr<int> p2(p1);即用p1拷贝构造p2时是浅拷贝,赋值运算同理。


c++98中正式给出了智能指针的概念。那c++98是怎么解决浅拷贝问题的呢?我们看下面的代码,

#include<memory>
int main()
{
    auto_ptr<int> p1(new int);
    *p1 = 1;
    auto_ptr<int> p2(p1);
    cout << *p1 << endl;
    cout << *p2 << endl;
    system("pause");
    return 0;
}

它在运行时会崩溃,造成这种崩溃的原因正是这一版本中智能指针处理浅拷贝的方式,它在处理拷贝构造函数和赋值运算符重载时把使用的是资源转移的方式,把之前的资源转移给新对象,之前置为空,所以在解引用访问p1时就会崩溃,我们可以简单实现一下这种方式:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p = nullptr)
        :_ptr(p)
    {}
    ~SmartPtr()
    {
        if (_ptr){
            delete _ptr;
        }
    }
    SmartPtr(SmartPtr<T> & p)
        :_ptr(p._ptr)
    {
        p._ptr = nullptr;

    }
    SmartPtr<T>& operator=(SmartPtr<T> & p)
    {
        if (this !=& p)
        {
            if (_ptr){//如果_ptr内有资源则先将自己的资源释放,否则会造成内存泄漏
                delete _ptr;
            }
            _ptr = p._ptr;
            p._ptr = nullptr;
        }
        return *this;
    }
    T &operator*()
    {
        return *_ptr;
    }
    T * operator->()
    {
        return _str;
    }
private:
    T* _ptr;
};

int main()
{
    SmartPtr<int> p1(new int);
    SmartPtr<int> p2(p1);
    SmartPtr<int> p3(new int);
    SmartPtr<int> p2 = p3;//体现赋值运算符第二个if的作用

    system("pause");
    return 0;
}

但上面的方式有个缺陷是将之前的指针与内存断开联系后就不能在对其进行操作,有趣的是在c++03版本中对解决auto_ptr的浅拷贝有了新的方式:新增一bool类型成员变量_owner记录当前对象是否有权限释放内存,我们同样可以简单实现一下这个版本的智能指针:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T * p = nullptr)
        :_ptr(p)
        , _owner(false)
    {
        if (_ptr){
            _owner = true;
        }
    }

    ~SmartPtr()
    {
        if (_ptr&&_owner){
            delete _ptr;
            _ptr = nullptr;
        }
    }

    SmartPtr(SmartPtr<T> & p)
        :_ptr(p._ptr)
        , _owner(p._owner)//将_owener更新,所以拷贝构造只释放_owner为true的
        //解决了浅拷贝的问题
    {
        p._owner = false;
    }
    SmartPtr<T>& operator=(SmartPtr<T> & p)
    {
        if (this != &p){
            //如果当前对象管理了资源先把它释放
            if (_ptr){
                delete _ptr;
            }
            _ptr = p._ptr;//资源转移
            _owner = p._owner;//释放权限转移
            p._owner = false;
        }
    }
    T &operator*()
    {
        return *_ptr;
    }
    T * operator->()
    {
        return _str;
    }
private:
    T* _ptr;
    bool _owner;
};

用这种方式解决浅拷贝就可以对同时对之前的指针进行操作了,但造成了更大的缺陷,如果使用如下代码会怎么样呢?

int main()
{
    SmartPtr<int> p1(new int);
    if (1){
        SmartPtr<int> p2(p1);
        *p2 = 10;
    }
    //p1是野指针
    *p1 = 20;

    system("pause");
    return 0;
}

由于p1和p2共用同一块内存空间,在出if作用域后p2将调用其析构函数完成对资源的释放,所以p1变成野指针,为我们的代码造成了隐患,这种危害其实更严重,所以在c++11版本中又将智能指针的实现回退到了最开始的RAII模式,并给出了unique_ptr这种并且将拷贝构造函数和赋值运算符重载这两个默认成员函数禁用,也就不会有浅拷贝的发生了。

#include<memory>

int main()
{
    unique_ptr<int> p1(new int);
    //报错
    unique_ptr<int> p2(p1);
    unique_ptr<int> p3(new int);
    //报错
    p3 = p1;
    system("pause"); 
    return 0;
}

这种方式简单粗暴,我们可以想象一下它的内部实现原理是什么样的,在c++98中我们可以将拷贝构造函数和赋值运算符重载这两个函数给成私有成员函数在类外就无法调用也就避免了浅拷贝的问题。

private:
        unique_ptr(unique_ptr<T> & p){};
        unique_ptr<T> &operator=(unique_ptr<T> &p){};

c++11中将delete关键字的作用进行了扩展,作用是跟在默认构造函数后可以禁止调用这个函数:

unique_ptr(const unique_ptr<T> &p) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

而在c++98中提供了可以共享资源的智能指针:shared_ptr,它是增加了计数,也就是利用写时拷贝解决浅拷贝问题的。具体看代码及注释:

template<class T>
class DFDef
{
public:
    void operator()(T*&ptr){
        if (ptr){
            delete ptr;
            ptr = nullptr;
        }
    }
};
namespace Mine
{
    template<class T,class DF=DFDef<T>>
    class shared_ptr
    {
    public:
        shared_ptr(T*ptr = nullptr)
            :_ptr(ptr)
            , _pCount(nullptr)
        {
            if (_ptr){
                _pCount = new int(1);//当_ptr不为空时给_pCount赋值为1
            }
        }

        ~shared_ptr()
        {
            //当计数>0时只给_pCount--
            //当计为0时说明当前资源已是最后一个对象在使用,此时由当前对象释放资源
            if (_ptr && 0 == --(*_pCount)){
                //delete _ptr;
                DF df;
                df(_ptr);
                delete _pCount;
            }
        }

        T& operator *()
        {
            return *_ptr;
        }

        T*operator->()
        {
            return _ptr;
        }

        //拷贝构造函数和赋值运算符的重载就需要考虑计数的问题了
        shared_ptr(const shared_ptr<T> &sp)
            :_ptr(sp._ptr)
            , _pCount(sp._pCount)
        {
            if (_ptr){
                ++(*_pCount);
            }
        }

        shared_ptr<T> & operator=(const shared_ptr<T> &sp)
        {
            if (this != &sp){
                //1.首先与旧资源断开联系(如果不是最后一个使用资源的对象就只让计数减一,如果是最后一个使用资源的对象则释放资源)
                if (_ptr && 0 == -(*_pCount)){
                    delete _ptr;
                    delete _pCount;
                }
                //2.与sp共享资源和计数
                _ptr = sp._ptr;
                _pCount = sp._pCount;
                if (_ptr){
                    ++*_pCount;
                }
            }
            return *this;
        }

        int use_count()
        {
            return *_pCount;
        }
    private:
        T*_ptr;
        int *_pCount;
    };
}

void TestShradPtr()
{
    Mine::shared_ptr<int> sp1(new int);
    cout << sp1.use_count() << endl;

    Mine::shared_ptr<int> sp2(sp1);
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;

    Mine::shared_ptr<int> sp3(new int);
    cout << sp3.use_count() << endl;

    Mine::shared_ptr<int> sp4(sp3);
    cout << sp3.use_count() << endl;
    cout << sp4.use_count() << endl;

    sp3 = sp2;
    cout << sp2.use_count() << endl;
    cout << sp3.use_count() << endl;

    sp4 = sp2;
    cout << sp2.use_count() << endl;
    cout << sp4.use_count() << endl;
}

int main()
{
    TestShradPtr();
    system("pause");
    return 0;
}

但是shared_pt在循环引用的时候可能会导致资源泄露,比如我们创建一个循环链表,其中的_pre和_next都是shared_ptr类型的,那么将这两个节点进行首位相连后会发生下面的情况:
*1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。

  1. node1的next指向node2,node2的prev指向node1,引用计数变成2。
  2. node1和node2析构,引用计数减到1,但是next还指向下一个节点。但是prev还指向上一个节点。
  3. 也就是说next析构了,node2就释放了。
  4. 也就是说prev析构了,node1就释放了。
  5. 但是next属于node的成员,node1释放了,next才会析构,而node1由prev管理,prev属于node2
    成员,所以这就叫循环引用,谁也不会释放。*

所以这两个节点谁都不会释放资源,就导致了资源的泄露。
如何解决这个问题,c++又给提供了weak_ptr型的智能指针,只需要将_pre和_next的指针类型改为weak_ptr即可,原理是weakptr不会增加node1和node2的引用计数。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章