我们都知道,C和C++常被人诟病的一点就是程序员需要自己来维护对动态内存的申请和释放,具体来说就是malloc/free和new/delete的成对出现。能够保证它们成对出现,是一个良好的编程习惯,但是即使做到这一点,就能够保证万无一失吗?我们看一个例子:
我们先定义一个简单的类A:
class A
{
public:
int a;
inline void print(){cout << a << endl;}
};
A * ptr = new A;
...
delete ptr;
正常情况下,这段代码顺利执行则不会出现问题。但是当在执行delete操作之前如果程序抛出异常呢?这样就会导致delete不能够执行,进而导致内存泄露。解决办法很多,比如使用异常来控制,但是这样就会显得逻辑混乱,而且不够优美。这里我们介绍下智能指针(smart pointer)。
C++中包含众多的智能指针,比如STL中的auto_ptr,boost库中的shared_ptr,weak_ptr,scoped_ptr,intrusive_ptr等。我们一一介绍。
1.智能指针之auto_ptr
auto_ptr是包含在标准库中,只需要#include <memory>头文件即可。我们先看下auto_ptr的用法:
auto_ptr<A> ptr(new A);
ptr->print();
可以看出,使用智能指针后代码简洁了很多,只管分配内存,而不用考虑释放的问题。看到这里,你也许想知道,智能指针到底是怎么做到资源自动智能释放的?
可以一句话解释:智能指针利用栈上分配对象的析构函数会自动执行而释放资源。
也许你还是不清楚,我们看一个简单版本智能指针的实现方案:
template<typename T>
class auto_ptr
{
public:
auto_ptr<T *p>{ptr = p;}
~auto_ptr(){delete ptr;}
private:
T *ptr;
};
从auto_ptr的实现,可以看出我们把资源的释放放到析构函数中,而无论程序怎样执行,析构函数是一定会执行的,因此就能够保证资源的正常回收。
但是,auto_ptr毕竟是比较初级的智能指针,比如它有以下缺陷:
1)auto_ptr不能共享所有权,当把一个智能指针ptr1赋值给另外一个智能指针ptr2时,原来的ptr1便失去了初始对象的控制权,而移交给了ptr2。
2)auto_ptr不能指向数组。
3)由于不支持赋值和拷贝,不能把auto_ptr放入到容器中。
至于为什么会有这些缺陷,跟auto_ptr的实现有关,具体可以参考标准的auto_ptr实现方式。
下面我们介绍更加智能的智能指针shared_ptr。
2.智能指针之shared_ptr
shared_ptr在boost库中被实现,因此使用的前提是包含boost库。从名字我们就可以大概窥之一二,该智能指针解决了所有权的共享问题,当把一个智能指针ptr1赋值给另外一个智能指针ptr2时,ptr1和ptr2同时拥有原指针的控制权,而不会出现auto_ptr的ptr1所有权丢失问题。那么,shared_ptr是如何做到这一点的呢?答案是:引用计数。说到引用计数大家应该不会陌生,在操作系统的内存管理中便存在这个概念,还有C++中string的实现,有些也采用了引用计数的方法,从而诞生了copy-on-write的技术,提高了效率。引用计数,说白了,就是采用一个计数器来维护当前该变量的引用数,当引用数变为0的时候,表示没有其他引用,就可以把对象进行销毁了。
除了具有上述优点,shared_ptr还有几个很好的特性:比如可以把shared_ptr放入标准容器中,shared_ptr可以指定除了代理对象外的第二个参数-删除器,用来自定义资源的释放方式。
要想使用shared_ptr,需要包含头文件#include "boost/shared_ptr.hpp"。一般来说,shared_ptr是最推荐,也是最常用的智能指针,它基本上能够满足我们的需求。
3.智能指针之weak_ptr
既然shared_ptr已经如此好用了,那么为什么还需要weak_ptr呢?我们先看一个例子:
shared_ptr<A> ptrA(new A);
cout << "c=" << ptrA.use_count() << endl; //输出 1
shared_ptr<A> ptrA2 = ptrA;
cout << "c=" << ptrA.use_count() << endl; //输出 2
weak_ptr<A> ptrA3 = ptrA;
cout << "c=" << ptrA.use_count() << endl; //输出 2
从上面的例子可以看出,使用shared_ptr会对引用计数加1,而weak_ptr则不会,它充当着一个旁观者的角色:我只是一个弱引用,我不会负责内存管理的。这样做有上面好处呢?一句话解释: 可以解决循环引用以及悬挂指针的问题。解决循环引用是因为弱引用不会对引用进行计数,解决悬挂指针是因为在强引用进行资源释放的同时会把weak_ptr置为空,因此可以通过expired()进行检查,从而避免非法内存访问。
要想使用weak_ptr,需要包含头文件#include "boost/weak_ptr.hpp"
4.智能指针之scoped_tr
scoped_ptr比较简单,甚至比auto_ptr更加弱智,主要体现在:auto_ptr还能够进行所有权的转移,而scoped_ptr则不行,不能够对scoped_ptr进行赋值或复制,它就只能够完成资源的自动释放而已。
要想使用scoped_ptr,需要包含头文件#include "boost/scoped_ptr.hpp"