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