条款十四:在资源管理类中小心copying行为
首先来看一个例子:
1 #include <iostream> 2 using namespace std; 3 4 class Lock 5 { 6 public: 7 explicit Lock(int* pm): m_p(pm) 8 { 9 lock(m_p); 10 } 11 12 ~Lock() 13 { 14 unlock(m_p); 15 } 16 17 private: 18 int *m_p; 19 void lock(int* pm) 20 { 21 cout << "Address = " << pm << " is locked" << endl; 22 } 23 24 void unlock(int *pm) 25 { 26 cout << "Address = " << pm << " is unlocked" << endl; 27 } 28 }; 29 30 31 int main() 32 { 33 int m = 5; 34 Lock m1(&m); 35 }
这个是模仿原书中的例子,做的一个加锁和解锁的操作。
运行结果如下:
这符合预期,当m1获得资源的时候,将之锁住,而m1生命周期结束后,也将资源的锁释放。
注意到Lock类中有一个指针成员,那么如果使用默认的析构函数、拷贝构造函数和赋值运算符,很可能会有严重的bug。
我们不妨在main函数中添加一句话,变成下面这样:
1 int main() 2 { 3 int m = 5; 4 Lock m1(&m); 5 Lock m2(m1); 6 }
再次运行,可以看到结果:
可见,锁被释放了两次,这就出问题了。原因是析构函数被调用了两次,在main()函数中生成了两个Lock对象,分别是m1和m2,Lock m2(m1)这句话使得m2.m_p = m1.m_p,这样这两个指针就指向了同一块资源。根据后生成的对象先析构的原则,所以m2先被析构,调用他的析构函数,释放资源锁,但释放的消息并没有通知到m1,所以m1在随后析构函数中,也会释放资源锁。
如果这里的释放不是简单的一句输出,而是真的对内存进行操作的话,程序就会崩溃。
归根到底,是程序使用了默认了拷贝构造函数造成的(当然,如果使用赋值运算的话,也会出现相同的bug),那么解决方案就是围绕如何正确摆平这个拷贝构造函数(和赋值运算符)。
第一个方法,很简单直观,就是干脆不让程序员使用类似于Lock m2(m1)这样的语句,一用就报编译错。这可以通过自己写一个私有的拷贝构造函数和赋值运算符的声明来解决。注意这里只要写声明就行了(见条款6)。
1 class Lock 2 { 3 public: 4 explicit Lock(int* pm): m_p(pm) 5 { 6 lock(m_p); 7 } 8 9 ~Lock() 10 { 11 unlock(m_p); 12 } 13 14 private: 15 int *m_p; 16 void lock(int* pm) 17 { 18 cout << "Address = " << pm << " is locked" << endl; 19 } 20 21 void unlock(int *pm) 22 { 23 cout << "Address = " << pm << " is unlocked" << endl; 24 } 25 26 private: 27 Lock(const Lock&); 28 Lock& operator= (const Lock&); 29 };
这样编译就不会通过了:
当然也可以像书上写的一样,写一个Uncopyable的类,把它作为基类。在基类中把它的拷贝构造函数和赋值运算写成私有的(为了防止生成基类的对象,但又想允许派生类生成对象,可以把构造函数和析构函数的修饰符变成protected)。
然后
class Lock: public Uncopyable
{…}
也就OK了
第二个方法,就是使用shared_ptr来进行资源管理(见前一个条款),但还有一个问题,我想在生命周期结束后调用Unlock的方法,其实shared_ptr里面的删除器可以帮到我们。
只要这样子:
1 class Lock 2 { 3 public: 4 explicit Lock(int *pm): m_p(pm, unlock){…} 5 private: 6 shared_ptr<int> m_p; 7 }
这样在Lock的对象的生命周期结束后,就可以自动调用unlock了。
在条款十三的基础上,我改了一下自定义的shared_ptr,使之也支持删除器的操作了,代码如下:
1 #ifndef MY_SHARED_PTR_H 2 #define MY_SHARED_PTR_H 3 4 #include <iostream> 5 using namespace std; 6 7 8 typedef void (*FP)(); 9 10 template <class T> 11 class MySharedPtr 12 { 13 14 private: 15 T *ptr; 16 size_t *count; 17 FP Del; // 声明一个删除器 18 static void swap(MySharedPtr& obj1, MySharedPtr& obj2) 19 { 20 std::swap(obj1.ptr, obj2.ptr); 21 std::swap(obj1.count, obj2.count); 22 std::swap(obj1.Del, obj2.Del); 23 } 24 25 public: 26 MySharedPtr(T* p = NULL): ptr(p), count(new size_t(1)),Del(NULL){} 27 28 // 添加带删除器的构造函数 29 MySharedPtr(T* p, FP fun): ptr(p), count(new size_t(1)), Del(fun){} 30 31 32 MySharedPtr(MySharedPtr& p): ptr(p.ptr), count(p.count), Del(p.Del) 33 { 34 ++ *p.count; 35 } 36 37 38 39 MySharedPtr& operator= (MySharedPtr& p) 40 { 41 if(this != &p && (*this).ptr != p.ptr) 42 { 43 MySharedPtr temp(p); 44 swap(*this, temp); 45 } 46 return *this; 47 } 48 49 ~MySharedPtr() 50 { 51 if(Del != NULL) 52 { 53 Del(); 54 } 55 reset(); 56 } 57 58 T& operator* () const 59 { 60 return *ptr; 61 } 62 63 T* operator-> () const 64 { 65 return ptr; 66 } 67 68 T* get() const 69 { 70 return ptr; 71 } 72 73 void reset() 74 { 75 -- *count; 76 if(*count == 0) 77 { 78 delete ptr; 79 ptr = 0; 80 delete count; 81 count = 0; 82 //cout << "真正删除" << endl; 83 } 84 } 85 86 bool unique() const 87 { 88 return *count == 1; 89 } 90 91 size_t use_count() const 92 { 93 return *count; 94 } 95 96 97 friend ostream& operator<< (ostream& out, const MySharedPtr<T>& obj) 98 { 99 out << *obj.ptr; 100 return out; 101 } 102 103 }; 104 105 106 107 108 109 #endif /* MY_SHARED_PTR_H */
第三个方法是复制底部资源,就是将原来的浅拷贝转换成深拷贝,需要自己显示定义拷贝构造函数和赋值运算符。这个也在之前的条款说过了,放到这里,其实就是在拷贝的时候对锁的计数次数进行+1,析构函数里就是对锁的计数次数进行-1,如果减到0就去unlock(其实思想还是类似于shared_ptr进行资源管理)
第四个方法,是转移底部资源的控制权,这就是auto_ptr干的活了,在第二个方法中把shared_ptr换成auto_ptr就行了。
最后总结一下:
- 复制RAII对象必须一并复制它所管理的资源,所以资源copying行为决定RAII对象的copying行为
- 普遍而常见的RAII class copying行为是:抑制copying,施行引用计数法(shared_ptr思想),或者是转移底部资源(auto_ptr思想)