对指针说拜拜。承认吧,你从未真正喜欢过它,对不?
好,你不需要对所有指针说拜拜,但是你真的得对那些用来操控局部性资源(local resources)的指针说莎唷娜拉了。
举个例子,你正在为“小动物收养保护中心”(一个专门为小狗小猫寻找收养家庭的组织)编写一个软件。收养中心每天都会产生一个文件,其中有它所安排的当天收养个案。你的工作就是写一个程序,读这些文件,然后为每一个收养个案做适当处理。
合理的想法是定义一个抽象基类(abstract base class)ALA("Adorable Litle Animal"),再从中派生出针对小狗和小猫的具体类(concrete classes)。其中有个虚函数processAdoption,负责“因动物种类而异”的必要处理动作。
class ALA{
public:
virtual void processAdoption()= 0;
...
};
class Puppy: public ALA
{
public:
virtual void processAdoption();
...
};
class Kitten: public ALA
{
public:
virtual void processAdoption ();
...
};
你需要一个函数,读取文件内容,并视文件内容产生一个Puppyobject或一个Kitten object。这个任务非常适合用virtual constructor完成,那是条款25中描述的一种函数。
对本目的而言,以下声明便是我们所需要的:
//从s读取动物信息,然后返回一个指针,指向一个
// 新分配的对象,有着适当的类型(Puppy或Kitten)
ALA * readALA(istream& s);
你的程序核心大约是一个类似这样的函数:
void processAdoptions(istream& dataSource)
{
while (datasource)//如果还有数据,
{
ALA* pa = readALA(dataSource);//取出下一只动物,
pa->processAdoption();//处理收养事宜,
delete pa;//删除readALA返回的对象。
}
}
这个函数走遍dataSource,处理它所获得的每一条信息。
唯一需要特别谨慎的是,它必须在每次迭代的最后,记得将pa删除。这是必要的,因为每当readALA被调用,便产生一个新的 heap object。如果没有调用 delete,这个循环很快便会出现资源泄漏的问题。
现在请考虑:如果pa->processAdoption抛出一个exception,会发生什么事情。processAdoptions 无法捕捉它,所以这个 exception 会传播到 processAdoptions的调用端。processAdoptions 函数内“位于pa->processAdoption 之后的所有语句”都会被跳过,不再执行,这也意味pa不会被删除。结果呢,只要pa->processAdoption 抛出一个 exception,processAdoptions 便发生一次资源泄漏。
要避免这一点,很简单:
void processAdoptions(istream& dataSource)
{
while (dataSource)
{
ALA* pa = readALA(dataSource),
try {
pa->processAdoption();
}
catch (...)//捕捉所有的exceptions。
{
delete pa;//当exception 被抛出,避免资源泄漏。
throw;//将exception 传播给调用端。
}
delete pa;//如果没有exception被抛出,也要避免资源泄漏。
}
}
但你的程序因而被try 语句块和catch 语句块搞得乱七八糟。
更重要的是,你被迫重复撰写其实可被正常路线和异常路线共享的清理代码(cleanup code)本例指的是delete动作。
这对程序的维护造成困扰,撰写时很烦人,感觉也不理想。不论我们是以正常方式或异常方式(抛出一个exception)离开processAdoptions函数,我们都需要删除pa,那么何不集中于一处做这件事情呢?
其实不必大费周章,只要我们能够将“一定得执行的清理代码”移到processAdoptions函数的某个局部对象的destructor 内即可。因为局部对象总是会在函数结束时被析构,不论函数如何结束(唯一例外是你调用longjmp而结束。longjmp 的这个缺点正是C++ 支持exceptions的最初的主要原因)。于是,我们真正感兴趣的是,如何把delete 动作从 processAdoptions 函数移到函数内某个局部对象的destructor内。
解决办法就是,以一个“类似指针的对象”取代指针pa,如此一来,当这个类似指针的对象被(自动)销毁,我们可以令其destructor 调用delete。
“行为类似指针(但动作更多)”的对象我们称为smart pointers。如条款28所言,你可以做出非常灵巧的“指针类似物”。本例倒是不需要什么特别高档的产品,我们只要求它在被销毁(由于即将离开其scope)之前删除它所指的对象,就可以啦。
技术上这并不困难,我们甚至不需要自己动手。C++标准程序库(见条款E49)提供了一个名为auto_ptr的class template,其行为正是我们所需要的。每个auto_ptr的constructor 都要求获得一个指向 heap object 的指针;其destructoy会将该heapobject 删除。
如果只显示这些基本功能,auto_ptr 看起来像这样:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* p = 0) :ptr(p) {}// 存储对象。
~auto ptr()
{
delete ptr;// 删除对象。
}
private:
T* ptr;//原始指针(指向对象).
};
auto_ptr 标准版远比上述复杂得多。上述这个剥掉一层皮的东西并不适合实际运用2(至少还需加上copy constructor,assignment operator 及条款28所讨论的指针仿真函数operator*和operator->),但是其背后的观念应该很清楚了:以auto_ptr 对象取代原始指针,就不需再担心heap objects没有被删除一一即使是在exceptions被抛出的情况下。
注意,由于auto ptr destructor 采用“单一对象”形式的delete,所以auto ptr 不适合取代(或说包装)数组对象的指针。如果你希望有一个类似 autoptr的template可用于数组身上,你得自己动手写一个。不过如果真是这样,或许更好的选择是以vector 取代数组。
以auto_ptr对象取代原始指针之后,processAdoptions 看起来像这样:
void processAdoptions(istream& dataSource)
{
while (dataSource)
{
auto_ptr<ALA> pa(readALA(dataSource)
pa->processAdoption();
}
}
这一版和原先版本的差异只有两处。
- 第一,pa被声明为一个 auto_ptr<ALA>对象,不再是原始的 ALA*指针;
- 第二,循环最后不再有delete 语句。就这样啦,其他每样东西都没变,除了析构动作外,auto_ptr 对象的行为和正常指针完全一样。很简单,不是吗?
隐藏在auto ptr背后的观念一—以一个对象存放“必须自动释放的资源”,并依赖该对象的destructor 释放一—亦可对“以指针为本”以外的资源施行。
考虑图形界面(GUT)应用软件中的某个函数,它必须产生一个窗口以显示某些信息:
//此函数可能会在抛出一个 exception 之后发生资源泄漏问题。
void displayInfo(const Informations info)
{
WINDOW_HANDLE w(createwindow());
displayinfo in window corresponding to w,
destroyWindow(w);
}
许多窗口系统都有 C语言接口,运行诸如 createWindow 和 destroyWindow这类函数,取得或释放窗口(被视为一种资源)。如果在信息显示于的过程中发生exception,w所持有的那个窗口将会遗失,其他动态分配的任何资源也会遗失。
解决之道和先前一样,设计一个class,令其constructor 和destructor 分别取得资源和释放资源:
//这个class 用来取得及释放一个 window handle。
class WindowHandle {
public:
WindowHandle(WINDOW HANDLE handle) :w(handle) {}
~WindowHandle() {
destroyWindow(w);
}
operator WINDOW_HANDLE()
{
return w;//详述于下。
}
private:
WINDOW_HANDLE w;
//以下函数被声明为private,用以阻止产生多个 WINDOW HANDLE。
//条款28讨论了一个更弹性的做法。
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};
这看起来就像auto_ptr template 一样,只不过其赋值动作(assignment)和复制动作(copying)被明确禁止了(见条款E27)。
此外,它有一个隐式类型转换操作符,可用来将一个 windowHandle 转换为一个 WINDOW HANDLE。
这项能力对于WindowHandleobject 的实用性甚有必要,意味你可以像在任何地方正常使用原始的WINDOW_HANDLE一样使用一个 windowHandle(不过条款5也告诉你为什么应该特别小心隐式类型转换操作符)。
有了这个 WindowHandle class,我们可以重写 displayInfo如下:
// 此函数可以在exception 发生时避免出现资源泄漏问题。
void displayInfo(const Information& info)
{
WindowHandle w(createwindow());
display info in window corresponding to w;
}
现在即使 displayInfo函数内抛出 exception,createWindow 所产生的窗口还是会被销毁。
只要坚持这个规则,把资源封装在对象内,通常便可以在exceptions 出现时避免泄漏资源。
但如果exception 是在你正取得资源的过程中抛出的,例如在一个“正在抓取资源”的class constructor 内,会发生什么事呢?
如果exception 是在此类资源的自动析构过程中抛出的,又会发生什么事呢?此情况下constructors和destructors 是否需要特殊设计?
是的,它们需要,你可以在条款 10和条款 11中学到这些技术。