RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源的简单技术。在对象构造时获取资源,在对象生命周期内我们始终可以通过对象来访问这个资源,最后在对象析构的时候释资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

简单原理

  1. 智能指针就是一个类。
  2. 智能指针的成员变量包括一个指向动态资源的指针,因此智能指针可以对动态资源做管理。
  3. 智能指针的构造函数的参数就是指向动态资源的指针,这样我们在定义一个智能指针的同时就可以把要管理的资源交给智能指针。
  4. 智能指针的析构函数中编写了释放其管理的动态资源的代码(delete,free),这样在智能指针生命周期结束调用析构函数时就能把动态资源一并释放。
  5. 如果要通过访问这智能指针个动态空间,我们只需要在类里面重载*,->等运算符即可。

c++98-auto_ptr

简单实现

namespace bit
{
    template<class T>
    class auto_ptr
    {
    public:
	auto_ptr(T* ptr)
	    :_ptr(ptr)
	{}
	auto_ptr(auto_ptr<T>& sp)
	    :_ptr(sp._ptr)
	{
	    // 管理权转移
	    sp._ptr = nullptr;
	}
	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
	    // 检测是否为自己给自己赋值
	    if (this != &ap)
	    {
		// 释放当前对象中资源
		if (_ptr)
		    delete _ptr;
		// 转移ap中资源到当前对象中
		_ptr = ap._ptr;
		ap._ptr = NULL;
	    }
	    return *this;
	}
	~auto_ptr()
	{
	    if (_ptr)
	    {
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	    }
	}
	// 像指针一样访问动态资源
	T& operator*()
	{
	    return *_ptr;
	}
	T* operator->()
	{
	    return _ptr;
	}
    private:
	T* _ptr;
    };
}

int main() 
{
    bit::auto_ptr<int> p(new int);
    *p = 5;
    cout << *p << endl;
}

缺陷

从auto_ptr的实现中就可以看出来,当我们使用一个智能指针构造另一个智能指针,或者两个智能指针之间赋值的时候,实际上是资源管理权的转移(一方得到资源,另一方的指向资源的指针就得置为空)。也就是说多个auto_ptr无法管理同一个资源。

有读者可能会想,在赋值或者拷贝构造后不要把原来管理这个资源的智能指针中指向该资源的指针值为空,那样不就可以让多个智能指针管理同一个资源了吗?实际上这样会导致同一份资源析构两次。

可以看出auto_ptr是不成熟的,但是如果在库里删除它会让一些已存在的使用了它程序无法运行。为了避免类似auto_ptr这种尴尬的情况,后续委员会成员先在boost库(c++扩展库)写功能,如果好用的话才纳入c++标准库。boos库中就有新的好用的智能指针scoped_ptr和shared_ptr和weak_ptr等,他们在c++11中被纳入标准库。在标准库中,unique_ptr对应的是boost库中的scoped_ptr。


c++11-unique_ptr

简单实现

template<class T>
class unique_ptr
{
public:
    unique_ptr(T* ptr)
	:_ptr(ptr)
    {}
    ~unique_ptr()
    {
    if (_ptr)
    {
    cout << "delete:" << _ptr << endl;
    delete _ptr;
    }
    }
	// 像指针一样使用
	T& operator*()
    {
	return *_ptr;
    }
    T* operator->()
    {
	return _ptr;
    }
    unique_ptr(const unique_ptr<T>& sp) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
    T* _ptr;
};

缺陷

unique_ptr相对于auto_ptr来说比较安全,因为auto_ptr可能发生“智能指针的管理权被转移后依旧使用该智能指针访问资源从而导致野指针”的问题,而unique_ptr直接禁止了拷贝。

但是缺点显而易见,依旧不能多个智能指针管理同一份资源,且适用面比auto_ptr还窄,因为它无法拷贝。


c++11-shared_ptr

简单实现

这是类模版的头,T表示shared_ptr可以管理任意类型的资源


这是shared_ptr的成员:

  1. _ptr指向动态资源的指针。
  2. _count是指向一个计数器。管理同一个资源的智能指针中,这个指针指向的计数器相同。这个计数器是动态开辟的,在资源释放的时候将他也释放。
  3. _delete是删除器。智能指针指向的资源不光是一个动态开辟的变量(用delete释放),也有可能是FILE类型(用fclose释放),还有可能是动态数组类型,用delete[]释放,总之释放资源的方式是不同的,但我们写的模版要尽可能泛化,所以我们在用智能指针管理资源的时候需要传入一个可调用对象(删除器),通过传入并调用不同的删除器,实现不同资源的正确释放。至于为什么用包装器(function<void(T*)>),我们稍后再说。

这是shared_ptr的构造函数,一旦调用构造函数,说明这是第一个管理该资源的智能指针,所以在构造函数中,需要给_count开辟空间,俗称:“第一个进入房间的人开灯”。

删除器类型有很多种:lambda表达式,仿函数,函数,我们用哪种类型储存这个删除器(可调用对象)呢?这时候就需要包装器了,它可以包装任何可调用对象,且只需要返回值和参数类型,参数类型就是指向资源的指针T*,返回值类型就是void因此我们使用包装器来存储删除器。


这是析构函数。智能指针析构时,要先让计数器--,然后判断是否需要释放资源,也就是count是否为0(即是否这是最后一个管理这个资源的智能指针)。是的话就调用release释放资源,不是的话就只让计数器--就可以了。俗称:“最后一个离开房间的人关灯”

而我把销毁逻辑封装在release函数里。


这是shared_ptr的关键,拷贝构造和赋值拷贝。

一但拷贝构造或者赋值,说明多了一个智能指针管理相同的资源,所以_count++。

对于赋值,先得把该智能指针原来管理的资源清理(复用 release),然后接收新的资源。


通过运算符重载,可以用访问指针的方式访问被管理的资源。


这个是比较好用的一种智能指针。它通过引用计数的方式解决拷贝问题。

原理:对于管理同一份资源的智能指针,不在采用管理权转移的方式,让他们都有管理权,这样会导致析构两次,所以我们再让这些智能指针管理同一个计数器,每多一个智能指针管理这个资源,计数器加1,每析构一个智能指针,计数器减1。如果计数器成为0,说明这个智能指针是唯一的管理者,此时如果它析构,我们就把资源也释放了。

这种方式防止了析构多次以及同一份资源不能被多个智能指针管理的缺陷,俗称“最后一个离开房间的人关灯”


缺陷-shared_ptr的循环引用

这是一个典型例子,下面是分析

这是shared_ptr的缺陷!

解决缺陷-weak_ptr

weak_ptr的简单实现如下:

把节点改成这样:

用weak_ptr代替节点中的shared_ptr,shared_ptr可以赋值给weak_ptr(通过weak_ptr的赋值拷贝),weak_ptr管理一个资源不会增加计数器。这样,x->_next = y,y->_prev = x 后,count1,count2还是1而不是2,此时在走一遍上述过程,你会发现循环引用已经破除了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐