1.2 C++中的健壮指针和资源管理
我最喜欢的堆资源的定义是:“任何在你的程序中获得并在此后释放的东西”,内存是一个相当明显的例子。它需要new来获得,用delete来释放,同时也有许多其它的类型资源文件句柄、重要片段、Windows中的GDI资源等等。
对于给定的资源的拥有者,是负责释放资源的一个对象或者是一段代码。所有权分为两种级别——自动的和显示的。如果一个对象的释放是由语言本身的机制来保证的,这个对象就是被自动的所有。
1.2.1 第一条规则(RAII)
一个指针,一个句柄,一个临界区状态只有在我们将他们封装入对象的时候才会拥有所有者。这就是我们的第一规则:在构造函数中分配资源,在析构函数中释放资源。当你按规则将所有资源封装的时候,你可以保证你的程序中没有任何的资源泄漏。这点在当封装对象在栈中建立或者嵌入在其他的对象中的时候非常的明显。但是对那些动态申请的对象呢?不要急!任何动态申请的东西都被看作一种资源,并且按照上面提到的方法进行封装。这一对象封装对象的链不得不在某个地方终止。它最终终止在一个高级的所有者,自动的或者是静态的。这些分别是对离开作用域或者程序时释放资源的保证。
下面是资源封装的一个经典例子。在一个多线程的应用程序中,线程直接共享对象的问题是通过用这样一个对象联系临界区来解决的。每一个需要访问共享资源的客户需要获得临界区。例如,这是Win32下临界区的实现方法。
class CritSect
{
friend class Lock;
public:
CritSect() { InitializeCriticalSection(&_critSection); }
~CritSect() { DeleteCriticalSection(&_critSection); }
private:
void Acquire() { EnterCriticalSection(&_critSection); }
void Release() { LeaveCriticalSection(&_critSection); }
private:
CRITICAL_SECTION _critSection;
};
class Lock
{
public:
Lock(CritSect& critSect): _critSect(critSect)
{
_critSect.Acquire();
}
~Lock()
{
_critSect.Release();
}
private:
CritSect& _critSect;
};
这里聪明的部分是我们确保每一个进入临界区的客户租后都可以离开。“进入”临界区的状态是一种资源,并应当被封装。封装器通常被称作一个锁(lock)。
注意无论发生什么,临界区都会借助于语言的机制保证释放。还有一件需要记住的事情——每一种资源都需要被分别封装。这是因为资源分配是一个非常容易出错的操作,是要资源是有限提供的。我们会假设一个失败的资源分配会导致一个异常——事实上,这会经常的发生。所以如果你想试图用一个石头打两只鸟的话,或者在一个构造函数中申请两种形式的资源,你可能就会陷入麻烦。只要想想在一种资源分配成功但另一种资源分配失败抛出异常时会发生什么。因为构造函数还没有全部完成,析构函数不可能被调用,第一种资源就会发生泄漏。这种情况可以非常简单的避免。
1.2.2 Smart Points
我们至今还没有讨论最常见类型的资源——用操作符new分配,此后用指针访问的一个对象。我们需要为每个对象分别定义一个封装类吗?(事实上,C++标准模板库已经有了一个模板类,叫做auto_ptr,其作用就是提供这种封装。我们一会儿再回到auto_ptr。)让我们从一个极其简单、呆板单安全的东西开始。看下面的Smart Pointer模板类,它十分坚固,甚至无法实现。
template <class T>
class SmartPointer
{
public:
~SmartPointer() { delete _p; }
T *operator->() { return _p; }
T const *operator->() const { return _p; }
protected:
SmartPointer() : _p(0) {}
explicit SmartPointer(T* p) : _p(p) {}
T* _p;
};
为什么我们把SmartPointer的构造函数设计为protected呢?如果我需要遵守第一条规则,那么我就必须这样做。资源——在这里是class T的一个对象,必须在封装器的构造函数中分配。但是我们不能只简单的调用new T,因为我不知道T的构造函数的参数。因为在原则上每一个T都有一个不同的构造函数;我需要为他定义另外一个封装器。模板的用处会很大,为每一个新的类,我们可以通过继承SmartPointer定义一个新的封装器,并且提供一个特定的构造函数。
class SmartItem : public SmartPointer<Item>
{
public:
explicit SmartItem(int i) : SmartPointer<Item>(new Item(i)) {}
};
为每一个类提供一个Smart Pointer真的值吗?说实话——不!他很有教学的价值,但是一旦你学会如何遵循第一规则的话,你就可以放松规则并使用一些高级的技术。这以技术是让SmartPointer的构造函数成为public,但是只是用它来做资源转换(Resource Transfer)我的意思是用new操作符的结果直接作为SmartPointer的构造函数的参数,像这样:
SmartPointer<Item> item (new Item(i));
这个方法明显需要自控性,不只是你,而且包括你的程序小组的每一个成员。他们都必须发誓除了作资源转换外不把构造函数用在其他用途。幸运的是这条规则很容易得到加强。只需要在源文件中查找所有的new即可。
1.2.3 Resource Transfer
到目前为止,我们所讨论的一直是生命周期在一个单独的作用域内的资源。现在我们要解决一个困难的问题——如何在不同的作用域间安全的传递资源。这一问题当你在处理容器问题的时候会变的十分明显。你可以动态的创建一串对象,将它存放至一个容器中,然后将它取出,并且在最终安排他们。为了能够让这安全的工作——没有泄露——对象需要改变其所有者。
这个问题一个非常显而易见的解决方法是使用Smart Pointer,无论是在加入容器前还是找到他们后。
template <class T>
T* SmartPointer<T>::Release()
{
T* pTmp = _p;
_p = 0;
return pTmp;
}
注意在Release调用后,SmartPointer就不再是对象的所有者——它内部的指针指向空。现在,调用Release都必须是一个负责的人并且迅速隐藏返回的指针到新的所有者对象中。在我们的例子中,容器调用了Release,比如这个Stack的例子:
void Stack::Push(SmartPointer <Item> & item) throw(char*)
{
if(_top == maxStack)
throw "Stack overflow";
__arr [_top++] = item.Release();
}
同样的,你也可以在你的代码中加强Release的可靠性。