首先为什么需要提出智能指针的概念,先看下这个例子
void remodel(std::string & str) { std::string * ps = new std::string(str); ... if (weird_thing()) throw exception(); str = *ps; delete ps; return; }
如果weird_thing()返回1,程序直接抛出异常,那么ps这个指针便不能被执行到,当程序执行完之后,ps指针本身所占的内存会被释放,但是ps所指的内存将不被释放。这就是所谓的内存泄漏,指针没了,但是所指向的内存没有被释放。
我们可以想到,如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
因此,要转换remodel()函数,应按下面3个步骤进行:
- 包含头义件memory(智能指针所在的头文件);
- 将指向string的指针替换为指向string的智能指针对象;
- 删除delete语句。
# include <memory> void remodel (std::string & str) { std::auto_ptr<std::string> ps (new std::string(str)); ... if (weird_thing ()) throw exception(); str = *ps; // delete ps; NO LONGER NEEDED return; }
(1)auto_ptr
templet<class T> class auto_ptr { explicit auto_ptr(X* p = 0) ; ... };
shared_ptr<double> pd; double *p_reg = new double; pd = p_reg; // not allowed (implicit conversion) pd = shared_ptr<double>(p_reg); // allowed (explicit conversion) shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion) shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
将常规类型的指针可以显示转换成智能指针,可以通过强制类型转换和传参两种形式,如上面所写的形式。
缺点:
1、不要使用auto_ptr保存一个非动态开辟空间的指针,因为在作用域结束的时候,会执行智能指针的析构函数,释放这块空间,但非动态的空间又无法释放;
//正确情况: int i=new int(1); //堆上的空间——动态开辟 auto_ptr<int> ap1(&i); //错误情况: int i=1; //栈上的空间 auot_ptr<int> ap2(&i);
2、不要使用两个auto_ptr指针指向同一个指针。因为如果是常规指针,就相当于两个指针指向同一个对象。但是如果是auto_ptr来说
int a=10; int *p=&a; auto_ptr p1(p); auto_ptr p2(p1);
此时,p1这个智能指针就无法管理p这块内存内存空间了,因为在auto_ptr的拷贝函数的定义中,将p1拷贝给p2之后,p1就是个空指针了,这样执行析构函数的时候,还会对p1进行析构,即使此时p1保存的是一个空指针,但是释放造成的开销也是不必要的
3、不要使用auto_ptr指向一个指针数组,因为auto_ptr的析构函数所用的是delete而不是delete[],不匹配;
4、不要将auto_ptr储存在容器中,因为赋值和拷贝构造后原指针无法使用。
(2)scoped_ptr
scoped_ptr没有给出拷贝构造和赋值运算符的重载运算符的定义,只给了private下的声明,即表明scoped_ptr智能指针无法使用一个对象创建另一个对象,也无法采用赋值的形式。这无疑提升了智能指针的安全性,但是又存在无法“++”、“–”这些操作,当然也多了“*”、“->”这两种操作。所以这种形式也并不是最完美的。所以又有了shared_ptr。
(3)shared_ptr
shared_ptr和以上二者的最大区别就是维护了一个引用计数,用于检测当前对象所管理的指针是否还被其他智能指针使用(必须是shared_ptr管理的智能指针),在析构函数时对其引用计数减一,判断是否为0,若为0,则释放这个指针和这个引用计数的空间。其实,这个原理就和string类的浅拷贝是一样的。
缺陷:当管理的每一个指针都是一个双向链表的指针时,那么此时我们的析构函数存在一个很大的问题。
现在我们假设一种最简单的情况,这个双向链表中只有两个节点,并且p1的prev和p2的next都指向空。但注意,这时p1本身管理一段空间,p2的prev也管理p1管理的这块空间,所以p1下的引用计数为2,在p1的析构函数时对其引用计数减一,发现并没有为0,所以选择不释放p1的空间和p1的引用计数的空间。这样就造成了内存泄漏。我们称之为:循环引用
所以,弱指针weak_ptr就应运而生了,其实在库中shared_ptr和weak_ptr这两个智能指针类都公有继承了一个抽象的引用计数的类,所以,shared_ptr和weak_ptr的实现方式所差无几,就是二者的引用计数有区别。
(4)weak_ptr
weak_ptr也维护了一个引用计数,跟shared_ptr维护的引用计数或互不干扰,或相互协同。weak_ptr的指针会在weak_ptr维护的引用计数上加一,而shared_ptr会在shared_ptr维护的引用计数上加一,这样在循环引用时,就会因为对不同引用的判断的不同,使最终决定是否释放空间的结果也不相同。具体方式在下面举例说明。
结构体中使用两个弱指针weak_ptr管理它的next和prev域,而这个节点本身为shared_ptr。这种用法使得作用域完毕执行析构函数时按如下方式执行:
这样析构的顺序将变成如下过程,可避免循环引用
p1(usecount=1,weakcount=2),p2(usecount=1,weakcount=2)——开始进入析构流程——首先析构p2指针本身,p1(usecount=1,weakcount=1),p2(usecount=0,weakcount=1)——然后只能开始析构p1指针本身,p1(usecount=0,weakcount=1),p2(usecount=0,weakcount=0)——可以释放p2的引用空间了,p1(usecount=0,weakcount=0),p2(usecount=0,weakcount=0)——最后p1的引用空间也可以被释放。