下面是资源封装的一个经典例子。在一个多线程的应用程序中,线程之间共享对象的问题是通过用这样一个对象联系临界区来解决的。每一个需要访问共享资源的客户需要获得临界区。
1: class CritSect 2: { 3: friend class Lock; 4: public: 5: CritSect () { InitializeCriticalSection (&_critSection); } 6: ~CritSect () { DeleteCriticalSection (&_critSection); } 7: private: 8: void Acquire () 9: { 10: EnterCriticalSection (&_critSection); 11: } 12: void Release () 13: { 14: LeaveCriticalSection (&_critSection); 15: } 16: private: 17: CRITICAL_SECTION _critSection; 18: };
这里聪明的部分是我们确保每一个进入临界区的客户最后都可以离开。"进入"临界区的状态是一种资源,并应当被封装。封装器通常被称作一个锁(lock)。
1: class Lock 2: { 3: public: 4: Lock (CritSect& critSect) : _critSect (critSect) 5: { 6: _critSect.Acquire (); 7: } 8: ~Lock () 9: { 10: _critSect.Release (); 11: } 12: private 13: CritSect & _critSect; 14: };
锁一般的用法如下:
1: void Shared::Act () throw (char *) 2: { 3: Lock lock (_critSect); 4: // perform action —— may throw 5: // automatic destructor of lock 6: }
注意无论发生什么,临界区都会借助于语言的机制保证释放。
还有一件需要记住的事情——每一种资源都需要被分别封装。这是因为资源分配是一个非常容易出错的操作,是要资源是有限提供的。我们会假设一个失败的资源分配会导致一个异常——事实上,这会经常的发生。所以如果你想试图用一个石头打两只鸟的话,或者在一个构造函数中申请两种形式的资源,你可能就会陷入麻烦。只要想想在一种资源分配成功但另一种失败抛出异常时会发生什么。因为构造函数还没有全部完成,析构函数不可能被调用,第一种资源就会发生泄露。
这种情况可以非常简单的避免。无论何时你有一个需要两种以上资源的类时,写两个小的封装器将它们嵌入你的类中。每一个嵌入的构造都可以保证删除,即使包装类没有构造完成。这是对需要管理多个资源的复杂对象来说的,下面的例子说明了这样情形,
1: class FileHandle { 2: public: 3: FileHandle(char const* n, char const* a) { p = fopen(n, a); } 4: ~FileHandle() { fclose(p); } 5: private: 6: // 禁止拷贝操作 7: FileHandle(FileHandle const&); 8: FileHandle& operator= (FileHandle const&); 9: FILE *p; 10: };
1: class Widget { 2: public: 3: Widget(char const* myFile, char const* myLock) 4: : file_(myFile), // 获取文件myFile 5: lock_(myLock) // 获取互斥锁myLock 6: {} 7: // ... 8: private: 9: FileHandle file_; 10: LockHandle lock_; 11: };
Widget类的构造函数要获取两个资源:文件myFile和互斥锁myLock。每个资源的获取都有可能失败并且抛出异常。FileHandle和LockHandle类的对象作为Widget类的数据成员,分别表示需要获取的文件和互斥锁。资源的获取过程就是两个成员对象的初始化过程。在此系统会自动地为我们进行资源管理,程序员不必显式地添加任何异常处理代码。例如,当已经创建完file_,但尚未创建完lock_时,有一个异常被抛出,则系统会调用file_的析构函数,而不会调用lock_的析构函数。
综合以上的内容,RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。
2.3 Smart pointers(智能指针)
在《C++内存管理技术内幕》中,是这么解释smart pointer的。
如果我们用操作符new来动态申请一个对象,此后用指针访问的一个对象。我们需要为每个对象分别定义一个封装类吗?让我们从一个极其简单、呆板但安全的东西开始。看下面的Smart Pointer模板类,它十分坚固,甚至无法实现。
1: template
为什么要把SmartPointer的构造函数设计为protected呢?如果需要遵守第一条规则,那么就必须这样做。资源——在这里是class T的一个对象——必须在封装器的构造函数中分配。但是不能只简单的调用new T,因为我不知道T的构造函数的参数。因为,在原则上,每一个T都有一个不同的构造函数;我需要为他定义个另外一个封装器。模板的用处会很大,为每一个新的类,我可以通过继承SmartPointer定义一个新的封装器,并且提供一个特定的构造