上一章讲到了如何使用auto_ptr和shared_ptr来管理基于堆(heap)的资源,但对于堆之外的资源,例如Mutex锁,智能指针就不再那么好用了,因此我们需要写自己的资源管理类。
那么假如我们现在就正在操作一个Mutex锁:
void lock(Mutex* pm); //锁住pm指向的锁
void unlock(Mutex* pm); //解锁pm指向的锁
同时我们有一个符合RAII规范的类来管理这些锁,RAII即获取资源在对象构造过程中,释放资源在对象析构过程中:
class Lock{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm)
{lock(mutexPtr);} //在构造时获取资源,上锁
~Lock(){ unlock(mutexPtr); } //在析构时释放资源,解锁
private:
Mutex* mutexPtr;
};
例如访问临界区(critical section), 临界区即线程必须互斥地访问某些资源,这些资源必须只能由最多一个线程访问,我们就需要以RAII的方式来进行操作:
Mutex m;
...
{ //创建一个代码块来定义临界区
Lock ml(&m); //构造锁ml,锁住m
... //执行临界区操作
} //临界区结束,调用ml的析构函数,解锁
到现在为止以上的用法都是没有问题的,可如果锁被拷贝了呢?
Lock ml1(&m); //m在ml1的构造过程中被锁住
Lock ml2(ml1); //把ml1拷贝进ml2,会发生什么?
在创建自己的RAII资源管理类时,我们必须要思考需要如何规定这个类的拷贝行为。对于这个问题,我们有如下选择:
- 禁止拷贝:
有些对象的拷贝是没有意义的,就比如栗子中的这个锁,没有人会给同一个资源上两个锁,对于这样的类,我们就干脆禁止掉拷贝。第6章我们讲到了需要把拷贝函数声明为私有来禁止掉拷贝,例如:
class Lock : private Uncopyable{ //见第6章
public:
... //与之前相同的定义
};
- 给资源引用计数:
有时我们需要一直持有一个资源直到最后一个对象使用完毕,要实现这样的功能,我们必须有一个计数器来统计当前有多少对象在使用这个资源。当生成一个拷贝时加一,当删除一个拷贝时减一,和shared_ptr是一样的原理。
我们可以替代裸指针把shared_ptr作为RAII对象的数据成员来实现这个功能,将mutexPtr的类型从Mutex*变成shared_ptr<Mutex>。我们知道默认下的shared_ptr在引用计数为零时会删除掉它所包含的指针,但对于Mutex锁,我们想要的是解锁而不是删除掉,否则我们是没有办法解开一个被删除的锁的。
不要着急,这只是默认的情况,shared_ptr提供了一个特殊的可定义函数,删除器(deleter),即在引用计数为零时调用的函数,是shared_ptr构造函数的一个附加参数。这个函数在auto_ptr中是不存在的,因此它不能有自定义的删除行为,只能删除掉它包括的指针。
class Lock{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm, unlock) //将unlock函数绑定到删除器
{lock(mutexPtr.get());}
//这里其实不需要定义析构函数
private:
std::shared_ptr<Mutex> mutexPtr; //使用shared_ptr,不使用裸指针
};
这里我们并没有定义析构函数,因为第5章讲到过,类的析构函数会调用它的非静态数据成员的析构函数。这个例子中,Lock类的析构函数会调用它的成员mutexPtr的析构函数,而在当mutexPtr的引用计数为零时,它的析构函数则会调用删除器,即我们绑定的unlock函数。(但最好还是在这里加上一个注释,告诉别人我们没有忘记析构函数,只是利用了C++的特性。)
- 深拷贝封装的资源:
有时候我们可以拥有某个资源的多份拷贝,那么我们的资源管理类就要确保每一份拷贝都要在使用周期结束后释放资源,并且每一份拷贝互不干涉,因此拷贝这样的对象就要拷贝它包含的所有资源,进行深拷贝(deep copy)。例如当对象包含一个指针,我们必须先生成一个指针的拷贝,分配一个新的内存空间再把数据拷贝过来,这就是深拷贝。如果是浅拷贝,拷贝则直接使用了本体的指针成员,没有生成指针的拷贝,那么两个对象的指针成员就会指向同一个地址,删除拷贝就会导致本体被删除。
- 转移所有权:
有时候我们想要只有一个对象来持有这个资源,因此进行拷贝的时候,资源的所有权就要从本体转移到拷贝上,本体不再持有资源,这也就是auto_ptr的原理。
第5章讲到编译器会为你生成默认的拷贝函数,即拷贝赋值运算符和拷贝构造函数,但除非它们能实现你想要的功能,我们需要写自己的拷贝函数来实现以上的其中一种功能。
总结:
- 拷贝RAII资源管理类的对象要根据它所包含的资源来具体考虑,我们想要资源展现出如何的拷贝行为,资源管理类就要展现出同样的拷贝行为。
- 常用的RAII类的拷贝行为有禁止拷贝,使用引用计数,拷贝资源,转移所有权,但也可以用其他做法来符合特殊需要。