资源及它们的所有权 我最喜欢的对资源的定义是:"任何在你的程序中获得并在此后释放的东西。"内存是 一个相当明显的资源的例子。它需要用new来获得,用delete来释放。同时也有许多其它 类型的资源文件句柄、重要的片断、Windows中的GDI资源,等等。将资源的概念推广到 程序中创建、释放的所有对象也是十分方便的,无论对象是在堆中分配的还是在栈中或 者是在全局作用于内生命的。 对于给定的资源的拥有着,是负责释放资源的一个对象或者是一段代码。所有权分立 为两种级别--自动的和显式的(automatic and explicit),如果一个对象的释放是由 语言本身的机制来保证的,这个对象的就是被自动地所有。例如,一个嵌入在其他对象 中的对象,他的清除需要其他对象来在清除的时候保证。外面的对象被看作嵌入类的所 有者。 类似地,每个在栈上创建的对象(作为自动变量)的释放(破坏)是在控制流离开了 对象被定义的作用域的时候保证的。这种情况下,作用于被看作是对象的所有者。注意 所有的自动所有权都是和语言的其他机制相容的,包括异常。无论是如何退出作用域的 --正常流程控制退出、一个break语句、一个return、一个goto、或者是一个throw--自 动资源都可以被清除。 到目前为止,一切都很好!问题是在引入指针、句柄和抽象的时候产生的。如果通过 一个指针访问一个对象的话,比如对象在堆中分配,C++不自动地关注它的释放。程序员 必须明确的用适当的程序方法来释放这些资源。比如说,如果一个对象是通过调用new来 创建的,它需要用delete来回收。一个文件是用CreateFile(Win32 API)打开的,它需要 用CloseHandle来关闭。用EnterCritialSection进入的临界区(Critical Section)需 要LeaveCriticalSection退出,等等。一个"裸"指针,文件句柄,或者临界区状态没有 所有者来确保它们的最终释放。基本的资源管理的前提就是确保每个资源都有他们的所 有者。 第一规则 一个指针,一个句柄,一个临界区状态只有在我们将它们封装入对象的时候才会拥有 所有者。这就是我们的第一规则:在构造函数中分配资源,在析构函数中释放资源。 当你按照规则将所有资源封装的时候,你可以保证你的程序中没有任何的资源泄露。 这点在当封装对象(Encapsulating Object)在栈中建立或者嵌入在其他的对象中的时 候非常明显。但是对那些动态申请的对象呢?不要急!任何动态申请的东西都被看作一 种资源,并且要按照上面提到的方法进行封装。这一对象封装对象的链不得不在某个地 方终止。它最终终止在最高级的所有者,自动的或者是静态的。这些分别是对离开作用 域或者程序时释放资源的保证。 下面是资源封装的一个经典例子。在一个多线程的应用程序中,线程之间共享对象的 问题是通过用这样一个对象联系临界区来解决的。每一个需要访问共享资源的客户需要 获得临界区。例如,这可能是Win32下临界区的实现方法。 代码: class CritSect { friend class Lock; public: CritSect () { InitializeCriticalSection (&_critSection); } ~CritSect () { DeleteCriticalSection (&_critSection); } private: void Acquire () { EnterCriticalSection (&_critSection); } void Release () { LeaveCriticalSection (&_critSection); } CRITICAL_SECTION _critSection; }; 这里聪明的部分是我们确保每一个进入临界区的客户最后都可以离开。"进入"临界区的 状态是一种资源,并应当被封装。封装器通常被称作一个锁(lock)。 代码: class Lock { public: Lock (CritSect& critSect) : _critSect (critSect) { _critSect.Acquire (); } ~Lock () { _critSect.Release (); } private: CritSect & _critSect; }; 锁一般的用法如下: void Shared::Act () throw (char *) { Lock lock (_critSect); // perform action -- may throw // automatic destructor of lock } 注意无论发生什么,临界区都会借助于语言的机制保证释放。 还有一件需要记住的事情--每一种资源都需要被分别封装。这是因为资源分配是一 个非常容易出错的操作,是要资源是有限提供的。我们会假设一个失败的资源分配会导 致一个异常--事实上,这会经常的发生。所以如果你想试图用一个石头打两只鸟的话, 或者在一个构造函数中申请两种形式的资源,你可能就会陷入麻烦。只要想想在一种资 源分配成功但另一种失败抛出异常时会发生什么。因为构造函数还没有全部完成,析构 函数不可能被调用,第一种资源就会发生泄露。 这种情况可以非常简单的避免。无论何时你有一个需要两种以上资源的类时,写两 个笑的封装器将它们嵌入你的类中。每一个嵌入的构造都可以保证删除,即使包装类没 有构造完成。 Smart Pointers 我们至今还没有讨论最常见类型的资源--用操作符new分配,此后用指针访问的一个 对象。我们需要为每个对象分别定义一个封装类吗?(事实上,C++标准模板库已经有了 一个模板类,叫做auto_ptr,其作用就是提供这种封装。我们一会儿在回到auto_ptr。 )让我们从一个极其简单、呆板但安全的东西开始。看下面的Smart Pointer模板类,它 十分坚固,甚至无法实现。 template <class T> class SPtr { public: ~SPtr () { delete _p; } T * operator->() { return _p; } T const * operator->() const { return _p; } protected: SPtr (): _p (0) {} explicit SPtr (T* p): _p (p) {} T * _p; }; 为什么要把SPtr的构造函数设计为protected呢?如果我需要遵守第一条规则,那么我就 必须这样做。资源--在这里是class T的一个对象--必须在封装器的构造函数中分配。但 是我不能只简单的调用new T,因为我不知道T的构造函数的参数。因为,在原则上,每 一个T都有一个不同的构造函数;我需要为他定义个另外一个封装器。模板的用处会很大 ,为每一个新的类,我可以通过继承SPtr定义一个新的封装器,并且提供一个特定的构 造函数。 class SItem: public SPtr<Item> { public: explicit SItem (int i) : SPtr<Item> (new Item (i)) {} }; 为每一个类提供一个Smart Pointer真的值得吗?说实话--不!他很有教学的价值,但 是一旦你学会如何遵循第一规则的话,你就可以放松规则并使用一些高级的技术。这一 技术是让SPtr的构造函数成为public,但是只是是用它来做资源转换(Resource Trans fer)我的意思是用new操作符的结果直接作为SPtr的构造函数的参数,像这样: SPtr<Item> item (new Item (i)); 这个方法明显更需要自控性,不只是你,而且包括你的程序小组的每个成员。他们都必 须发誓出了作资源转换外不把构造函数用在人以其他用途。幸运的是,这条规矩很容易 得以加强。只需要在源文件中查找所有的new即可。