您的位置:首页 > 运维架构

智能指针——AutoPtr & ScopedPer & SharedPtr & WeakPtr

2017-03-02 20:36 302 查看
在说智能指针之前先说一下什么是RAII,RAII就是资源分配即初始化,这是一种思想,其最广泛的应用就是智能指针。

智能指针就是能自动完成资源的清理和释放。

为什么需要智能指针呢?因为有时候写的代码忘了进行指针所指向内存的清理工作,或者有时候代码的运行会提前return或者throw使得本来已经写好的清理代码未曾运行,比如下面的代码:

void Test()
{
int* ptr = new int(0);

//1.throw
//2.return

delete ptr;
}


针对这种情况,当然是有办法来处理的,但是处理起来先对比较麻烦,对于抛异常的throw,可以去捕获异常然后进行释放空间,而用智能指针进行处理时则显得比较容易,因为智能指针只用定义,释放内存与清理工作交给了智能指针的模板类内的析构函数去完成,如此一来就显得方便许多,所以在C++11中就有ScopedPer (uniquePtr), SharedPtr 和 WeekPtr供我们使用,而AutoPtr是早期的一种智能指针,但是因为它是一个失败的设计,所以C++11中并没有收录,这里让我们来简单的看看这四种智能指针的实现机制。

(1)AutoPtr

AutoPtr是利用管理权的转移实现的,也就是说一个指针只管理一个内存,当前内存有别的指针指向的时候,原来指向这块内存指针就指向NULL,以此来实现智能管理,防止两次析构同一块内存或者有内存没有释放这种情况。

下面简单的看一下这种AutoPtr的模板类的模拟实现:

template<class T>
class AutoPtr
{
public:
AutoPtr()
:_Ptr(new T)
{}

AutoPtr(AutoPtr<T>& ap)
:_Ptr(ap._Ptr)
{
//将传过来的指针值赋值给当前对象,
//并将传过来的指针值赋值为空,防止产生野指针
ap._Ptr = NULL;
}

AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
//同样的处理
delete _Ptr;
_Ptr = ap._Ptr;
ap._Ptr = NULL;
}
return *this;
}
//  *(解引用) 和 ->(指针操作符)的重载实现类外的指针使用
T* operator->()
{
return _Ptr;
}

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

T* GetPtr()
{
return _Ptr;
}
private:
T* _Ptr;
};


说白了就是专门写了一个不用自己去调用的析构函数,所以这是一个失败的设计。

(2)ScoptedPtr( 或者叫UniquePtr )

ScoptedPtr是一种防拷贝的智能指针,或者说不允许进行拷贝和赋值更为准确,因为它是将它的拷贝构造和赋值运算符的重载在类内只声明,但是并不定义,所以在用别的指针进行赋值或者拷贝构造时,就会编译不过,也就保证了所写的代码中一个指针就只管理一块内存储,就不会发生内存泄漏等问题。

下面来看模拟实现的ScoptedPtr模板类代码:

template<class T>
class ScopedPtr
{
public:

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

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

T* GetPtr()
{
return _Ptr;
}
//将拷贝构造和赋值运算符的重载,
//在ScoptedPtr类内声明为保护,并且只声明不定义
protected:
ScopedPtr(ScopedPtr<T>& ap);
ScopedPtr& operator=(ScopedPtr<T>& ap);

T* _Ptr;

};


(3)SharedPtr

SharedPtr是利用引用计数实现的智能指针,引用计数就是在类的成员变量中再定义一个指针指向一块内存,这块内存中保存着指向_Ptr这个指针所管理的内存的指针个数,也就是说记录着有多少指针共同管理这块内存,当其中有指针不再指向这块内存时,只将引用计数减去1,当引用计数的值为1时,再将这块内存清理和释放,如此一来就实现了内存的重复利用和忘记释放的问题。

下面看一下Sharedptr的模拟实现:

template<class T>
class SharedPtr
{
public:
SharedPtr(T* ptr)
:_Ptr(ptr)
,_refcount(new size_t(1))
{}

SharedPtr(SharedPtr<T>& ap)
:_Ptr(ap._Ptr)
,_refcount(ap._refCount )
{
++(*_refcount);
}
~SharedPtr()
{
if (--(*_refcount) == 0)
{
delete _Ptr;
delete _refcount;
}
}

SharedPtr<T>& operator=(SharedPtr<T>& ap)
{
if (this != &ap)
{
Relase();
_Ptr = ap._Ptr;
_refcount = ap._refcount;
++(*_refcount);
}
}

void Relase()
{
delete _Ptr;
delete _refcount;
}
private:
T* _Ptr;                //智能指针
size_t* _refcount;      //引用计数
};


★★SharedPtr的循环引用问题:

我们看到了SharedPtr的引用计数模拟实现代码,在析构函数中更可以清晰地看到,只有当引用计数的值为1时才会对这个指针指向的内存进行清理和释放,在实际使用当中,有时候在需要析构它的时候,它的引用计数并没有减为1,这就使得内存泄漏。

来看下面的代码(实际使用需要包头文件):

struct ListNode
{
boost::shared_ptr<ListNode> _prev;   //前驱指针
boost::shared_ptr<ListNode> _next;   //后继指针
};

void Test()
{
boost::shared_ptr<ListNode> ptr1(new ListNode());
boost::shared_ptr<ListNode> ptr2(new ListNode());

ptr1->next = ptr2;      //ptr1的后继指针指向ptr2
ptr2->_prev = ptr1;     //ptr2的前驱指针指向ptr1

}


这里的代码产生了什么问题呢?我们来看一个简图:



我们看ptr1所指向的这块内存,由于是一个链表,所以在实际使用的过程中,它的后继节点会有指针指向它,也就是图中的绿色2号指针,如此一来它的引用计数就增加为2,而在析构的时候,由于ptr1是定义的智能指针,所以会自动完成清理工作,此时就是将它的引用计数值减去1,之后它的引用计数值变为1,对于ptr2指向的内存也是同样的道理,当ptr2 不在指向时,打的引用计数还是为1,如下图所示:



这里我们看到这块申请出来的内存就一直没有被析构,因为只有当它的引用计数值为1,并且要解除当前指针的指向关系时,它才会被析构,如图所示,左边的内存有ptr2->_prev指向,右边的内存有ptr1->_next指向,若想析构左边的内存,需要解除ptr2->_prev的指向,也就是ptr2->_prev被析构,若想析构右边的内存,需要解除ptr1->_next的指向,所以就陷入循环引用。

(4)WeakPtr

WeakPtr就是用来解决SharedPtr的循环引用问题的,它的实现原理是:只让指针指向,但是不增加引用计数。

也就是在使用的时候如下:

struct ListNode
{
boost::weak_ptr<ListNode> _prev;   //前驱指针
boost::weak_ptr<ListNode> _next;   //后继指针
};


总结:智能指针的只用要包含相关的头文件:



然后就可以像普通指针一样使用,而且不用自己现实的去完成清理和释放工作,非常方便。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: