C++:智能指针

首先,为什么需要智能指针?

来看下面这段代码

bool doSomething()
{

    // 如果时间执行失败了就返回false
    return false;
}

// 为了避免内存泄漏和文件描述符泄漏,我们需要写出以下这样冗余的代码
// 我们需要一种方法让他自动的释放掉
void Test1()
{
    int* p1 = new int[2];
    FILE* pf = fopen("test","r");
    // 1.如果打开文件失败,就需要释放p1的空间
    if(pf == NULL)
    {
        delete[] p1;
    }

    // 2.如果执行事件失败,就需要释放p1的空间,并且关闭pf文件
    if(!doSomething())
    {
        delete[] p1;
        fclose(pf);
        return;
    }

    // 3.如果抛出了异常,我们捕获异常以后,也需要释放p1的空间,并且关闭文件描述符
    try
    {
        throw 1;
    }

    catch(int err)
    {
        delete[] p1;
        fclose(pf);
        return;
    }

    // 4.逻辑正常结束,也需要释放空间和关闭文件
    delete[] p1;
    fclose(pf);
    return;
}

可以看到,在这段代码里,我们需要考虑一些异常情况,并且要非常非常注意释放资源。但是造成的问题就是,代码冗余,反复重复的释放代码,降低了可读性,并且反复多次的释放,如果有哪一个忘记写了或是漏写了,就会造成内存泄漏。

所以,这就体现出了智能指针的好处。

什么是智能指针

一个智能指针应具备以下的三点:

  • RAII
  • 像指针一样的使用
  • 拷贝构造与赋值
    先来说第一点:RAII
    后面两点后面会说

RAII

资源分配即初始化,定义一个类来封装资源的分配与释放,在构造函数完成资源的分配与初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。

再来看看都有哪些智能指针

auto_ptr

该智能指针是c98标准里规定的,需要注意的是,这个智能指针是不提倡使用的,某些情况下禁止使用的。所以只做了解。
上文种提到了智能指针的三点,再次讨论一下auto_ptr的后两点:
1.像指针一样使用,很容易就可以使用,重载*和->就可以实现
2.拷贝构造与赋值:拷贝构造默认的拷贝方式是浅拷贝,即把你的拷一份给我,对指针而言,就是你和我都指向同一空间,那么就会出现很大的问题,析构时会析构两次。auto_ptr对于该问题的解决方案是使用管理权转移的思想
这里写图片描述

我们来模拟实现一个auto_ptr:

template <class T>
class Auto_Ptr{
public:
    Auto_Ptr(T* ptr)
        :_ptr(ptr)
    {}
    ~Auto_Ptr()
    {
        if(_ptr!=NULL)
        {
            cout<<"delete _ptr"<<endl;
            delete _ptr;
        }
    }
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
    //以下做了管理权转移
    Auto_Ptr(Auto_Ptr<T>& ap)
        :_ptr(ap._ptr)
    {
        ap._ptr=NULL;
    }
    //ap3=ap2;
    T& operator=(Auto_Ptr<T>& ap)
    {
        if(this!=&ap)
        {
            if(_ptr!=NULL)
            {
                delete _ptr;
            }
            _ptr=ap._ptr;
            ap._ptr=NULL;
        }
        return *this;
    }

private:
    T* _ptr;
};

scoped_ptr

以下智能指针都是针对于Boots库中的智能指针。
与auot_ptr相似,scoped_ptr也具有以上三点,与auto_ptr不同的是对于拷贝构造与赋值实现思想不同。
scoped_ptr采用了防拷贝的思想,意思是,如果你要拷贝或是赋值但是我不允许这种行为。如果采取默认的拷贝构造与赋值,默认是浅拷贝,也就意味着我们必须实现一个,但是如何实现呢?scoped_ptr采取的方法是只声明不实现。并且将接口设置为私有的,也就说明你想实现也实现不了。

template <class T>
class Scoped_Ptr
{
public:
    Scoped_Ptr(T* ptr)
        :_ptr(ptr)
    {}
    ~Scoped_Ptr()
    {
        delete _ptr;
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
private:
    //防拷贝
    //只声明不实现
    //定义为私有的,也不允许你在外面实现
    Scoped_Ptr(const Scoped_Ptr<T>& sp);
    Scoped_Ptr<T>& operator=(const Scoped_Ptr<T>& sp);
private:
    T* _ptr;
};

shared_ptr

shared_ptr使用了一种我们比较熟悉的做法:引用计数
这里写图片描述

template<class T>
class Shared_Ptr
{
public:
    Shared_Ptr(T* ptr)
        :_ptr(ptr)
    {
        _pcount=new int(1);
    }
    ~Shared_Ptr()
    {
        if(--(*_pcount)==0)
        {
            delete _ptr;
            delete _pcount;
        }
    }
    Shared_Ptr(const Shared_Ptr& sp)
        :_ptr(sp._ptr)
         ,_pcount(sp._pcount)
    {
        ++(*_pcount);
    }
    //sp1=sp2;
    Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
    {
        if(_ptr!=sp._ptr)
        {
            if(--(*_pcount)==0)
            {
                delete _pcount;
                delete _ptr;
            }
            _ptr=sp._ptr;
            _pcount=sp._pcount;
            ++(*_pcount);
        }
        return *this;
    }
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
    int* _pcount;

};

这种做法乍一看是很完美,但是shared_ptr有一些问题:
1. 线程安全问题:由于对引用计数++不是原子的,所以这里是有线程安全问题的
2. 循环引用问题:这个问题使用以上问题是不会有问题的,比如在以下场景:

struct ListNode
{
    Shared_Ptr<ListNode> _next;
    Shared_Ptr<ListNode> _prev;


    ListNode()
        :_next(NULL)
         ,_prev(NULL)
    {}

    ~ListNode()
    {
        cout<<"~ListNode()"<<endl;
    }
};

void TestCycle()
{
    Shared_Ptr<ListNode> cur(new ListNode);
    Shared_Ptr<ListNode> next(new ListNode);

    cur->_next = next;
    next->_prev = cur;
}

这里写图片描述

那么该如何解决这个问题,我们不妨思考一下,造成的原因是由于引用计数,那么我们就不要让引用计数自增就好了。所以就需要另外一个智能指针出场–weak_ptr

weak_ptr

这个智能指针不含RAII,因为他的作用就是为了解决shared_ptr的循环引用问题的。
如何解决呢?看代码:

struct ListNode
{
    Weak_Ptr<ListNode> _next;
    Weak_Ptr<ListNode> _prev;


    ListNode()
        :_next(NULL)
         ,_prev(NULL)
    {}

    ~ListNode()
    {
        cout<<"~ListNode()"<<endl;
    }
};

void TestCycle()
{
    Shared_Ptr<ListNode> cur(new ListNode);
    Shared_Ptr<ListNode> next(new ListNode);

    cur->_next = next;
    next->_prev = cur;
}

将_next 与 _prev指针都定义为weak_ptr指针就可以避免出现循环引用问题了。

下面来总结一下以上提到的智能指针:

智能指针特点思想使用
auto_ptr带有缺陷管理权转移严禁使用
scoped_ptr简单粗暴防拷贝鼓励使用
shared_ptr相对复杂引用计数鼓励使用,注意循环引用问题
weak_ptr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值