智能指针可以帮助我们管理动态空间,即自动释放动态空间。
---------------------------------------------------------------------------------------------------------------------------------
简单原理
事实上,智能指针的原理就是将指向动态空间的指针托管给一个自定义对象(把这个指针作为成员变量),然后在析构函数中将动态空间释放。这样就实现了动态空间的生命周期与对象的生命周期相同。如果要使用这个指针,我们只需要在类里面重载*,->等运算符或者写成员函数即可。
---------------------------------------------------------------------------------------------------------------------------------
智能指针的历史及主要困难
智能指针在c++98中就已经被引入,即现在c++库中的auto_ptr类模版。但是它在拷贝时有一个缺陷:
为什么报错呢?本来我们想要x和y共同管理一块动态空间,但是因为 auto_ptr 中的拷贝实际上是管理权转移,也就是说,当把x拷贝给y的时候,x就没有了该动态空间管理权(存放动态空间指针的成员变量被置为空了),所以x->year++实际上是对空指针操作,所以会报错。
为什么非要实现拷贝呢?不让它拷贝行吗?不行,我们总有一些情况是需要拷贝的,比如说:
push_back中就会拷贝(赋值拷贝),但是拷贝就会产生上述问题。还有传参等情况。
那么库里面为什么不能不把x置为空?这样也不行,因为会导致被管理的指针析构两次而报错。
这就是智能指针的困难
可以看出auto_ptr是不成熟的,但是如果在库里删除它会让一些已存在的使用了它程序无法运行。为了避免像auto_ptr这种尴尬的情况,后续委员会成员先在boost库(c++扩展库)写功能,如果好用的话才纳入c++标准库。boos库中就有新的好用的智能指针shared_ptr,unique_str(简单粗暴禁止拷贝,适用面不广)等,他们在c++11中被纳入标准库。
C++11和boost中智能指针的关系
1. C++ 98 中产生了第一个智能指针auto_ptr.
2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的
---------------------------------------------------------------------------------------------------------------------------------
shared_ptr
这个是比较好用的一种智能指针。它通过引用计数的方式解决拷贝问题。
原理:对于管理同一份资源的智能指针,不在采用管理权转移的方式,让他们都有管理权,这样会导致析构两次,所以我们再让这些智能指针管理同一个计数器,每多一个智能指针管理这个资源,计数器加1,每析构一个智能指针,计数器减1。如果计数器成为0,说明这个智能指针是唯一的管理者,此时如果它析构,我们就把资源也释放了。
这种方式防止了析构多次以及同一份资源不能被多个智能指针管理的缺陷,俗称“最后一个离开房间的人关灯”
---------------------------------------------------------------------------------------------------------------------------------
接下来简单实现一下share_ptr
1.
这是类模版的头,T用来接收任意类型的资源
2.
_ptr指向存放动态资源的指针。
_count是指向计数器。管理同一个资源的智能指针中,这个指针指向的计数器相同。这个计数器是动态开辟的,在资源释放的时候将他也释放。我们给它缺省值为1。
_delete是删除器。智能指针指向的资源不光是一个动态开辟的变量(用delete释放),也有可能是FILE类型(用fclose释放),还有可能是动态数组类型,用delete[]释放,总之释放资源的方式是不同的,但我们写的模版要使用所有类型,所以我们在用智能指针管理资源的时候需要传入一个可调用对象(删除器),通过传入不同的删除器,实现资源的正确释放。至于为什么用包装器(function<void(T*)>),我们稍后再说。
3.
这是类模版的构造函数。
第一个函数是一个函数模版,D是函数模版参数,由你传入的对象类型决定,T是类模版参数,由类决定。我们知道删除器是要释放资源,而释放资源是在析构函数内部,也就是说,我们要在析构函数内部使用D类型的对象。在一个函数传入,在另一个函数使用,如何做呢?他两共有的可以访问的是成员变量,所以我们可以用成员变量来接收这一对象,然后另一个函数就可以使用了。
那么问题又来了,这个成员变量应该是什么类型呢?是D,但问题是,D是函数模版参数,类模版没有这个模版参数,它不认识D,这时候就需要包装器了,它可以包装任何可调用对象,且只需要返回值和参数类型,参数类型就是指向资源的指针T*,返回值类型就是void。
function<void(T*)> _delete = [](T* x)->void {delete x; };
而[](T* x)->void {delete x; }是一个lambda表达式,它作为删除器的缺省值,使得不传删除器的时候也可以有默认删除器使用。
至于为什么不直接把删除器设计成类模版参数,这是因为我们要模拟库的实现,哪种都可以的。
第二个函数是普通的构造函数。
4.
这是析构函数。智能指针析构时,要先让count--,然后判断是否需要释放资源,也就是count是否为0(即这是最后一个管理这个资源的智能指针)。
而我把逻辑封装在release函数里。
5.
这是shared_ptr的关键,拷贝构造和赋值拷贝。
一但拷贝构造或者拷贝赋值,说明多了一个智能指针管理相同的资源,所以_count++。
对于拷贝复制,先得把该智能指针原来管理的资源清理(复用 release),然后接收新的资源。
6.
这是关于被管理资源的使用函数。const需要加上,以便const对象和非const对象都能调用这些函数,可以自己写去体会一下为什么要加。
---------------------------------------------------------------------------------------------------------------------------------
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,此时在走一遍上述过程,你会发现循环引用已经破除了。
---------------------------------------------------------------------------------------------------------------------------------
对了,weak_ptr可以通过lock函数恢复到shared_ptr,这里只是简单介绍智能指针,说的不全面,见谅