想象你正在开发一个多媒体通信簿软件。这个软件可以放置包括人名、地址、电话号码等文字,以及一张个人相片和一段个人声音(或许是其姓名的发音)。
为实现此软件,你可能设计如下:
cpp
class Image {//给影像数据使用。
public:
Image(const string& imageDataFileName);
//...
};
class AudioClip {
public:
//给音频数据使用。
AudioClip(const string& audioDataFileName);
//...
};
class PhoneNumber { ... };// 用来放置电话号码。
class BookEntry {//用来放置通信簿的每一个个人数据。
public:
BookEntry(const string& name,
const string& address = "",
const string& imageFileName,
const stringe audioclipFileName = "");
~BookEntry();
// 电话号码通过此函数加入。
void addPhoneNumber(const PhoneNumber& number);
private:
string theName; // 个人姓名。
string theAddress; //个人地址。
list<PhoneNumber> thePhones;//个人电话号码。
Image* theImage;//个人相片。
AudioClip* theAudioclip;//一段个人声音。
};
每一个BookEntry 都必须有姓名数据,所以它必须成为一个constructor 自变量(见条款3),但是其他字段一---个人地址及相片文件和声音文件------都可有可无。注意,我利用1ist class放置个人电话号码,list是C++标准程序库(见条款E49和条款35)提供的数个容器类(container classes)之一。
BookEntry constructor 和 destructor 可以直截了当地这么设计:
cpp
BookEntry::BookEntry(const string& name,
const strings address,
const string& imageFileName,
const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(0), theAudioclip(0)
{
if (imageFileName != "")
{
theImage = new Image(imageFileName);
}
if (audioclipFileName != "") {
theAudioclip = new Audioclip(audioClipFileName);
}
}
BookEntry::~BookEntry()
{
delete theImage;
delete theAudioClipi
}
其中constructor 先将指针 theImage 和theAudioClip 初始化为null;
如果对应的自变量不是空字符串,再让它们指向真正的对象。
destructor 负责删除上述两个指针,确保 BookEntry object 不会造成资源泄漏问题。由于C++保证"删除null指针"是安全的,所以BookEntry destructor 不必在删除指针之前先检查它们是否真正指向某些东西。
每件事看起来都很好,正常情况下每件事也的确很好,但是在不正常的情况下------在exception 出现的情况下------事情一点也不好。
当程序执行 BookEntry constructor 的以下部分,如果有个 exception 被抛出,会发生什么事?
cpp
if (audioclipFileName != "")
{
theAudioClip = new AudioClip(audioclipFileName);
}
exception的发生可能是由于 operator new(见条款8)无法分配足够的内存给一个 Audioclip object 使用,也可能是因为AudioClip constructor 本身抛出一个exception。
不论原因为何,只要是在 BookEntry constructor 内抛出,就会被传播到正在产生 BookEntryobject的那一端。
现在,如果在产生"原本准备让 theAudioclip指向"的对象时,发生了一个exception,控制权因而移出 BookEntry constructor 之外,谁来删除theImage 已经指向的那个对象呢?
明显的答案是由 BookEntry destructor来执行,但是这个明显的答案是个错误答案。BookEntry的destructor 绝不会被调用,绝对不会。
**C++只会析构已构造完成的对象。对象只有在其constructor 执行完毕才算是完全构造妥当。**所以如果程序打算产生一个局部性的 BookEntry object b:
cpp
void testBookEntryClass()
{
BookEntry b("Addison-Wesley Publishing Company",
"One Jacob Way, Reading, MA 01867");
//...
}
**而exception在b的构造过程中被抛出,b的destructor就不会被调用。**如果你尝试更深入地参与,将b分配于heap 中,并在exception 出现时调用delete:
cpp
void testBookEntryClass()//注意这是类外
{
BookEntry* pb = 0;
try
{
pb = new BookEntry("Addison-Wesley Publishing Company"
"One Jacob Way, Reading, MA 01867");
}
catch (...) {//捕提所有的exceptions。
delete pb;//当exception 被抛出,删除pb。
throw;// 将exception 传给调用者。
}
delete pb;
//正常情况下删除pb。
}
你会发现BookEntry constructor 所分配的 Image object 还是泄漏了。因为除非new动作成功,否则上述那个 assignment(赋值)动作并不会施加于pb身上。
如果 BookEntry constructor 抛出一个 exception,pb 将成为null指针,此时在catch 语句块中删除它,除了让你感觉比较爽之外,别无其他作用。
以smart pointer class autoptr<BookEntry>(见条款9)取代原始的 BookEntry*,也不会让情况好转,因为除非new动作成功,否则对pb的赋值动作还是不会进行。
面对尚未完全构造好的对象,为什么C++拒绝调用其destructor呢?它可不是为了让你痛苦而做成这样的设计的。
是的,这是有理由的。如果那么做,许多时候会是一件没有意义的事,甚至是一件有害的事。如果destructor 被调用于一个尚末完全构造好的对象身上,这个destructor 如何知道该做些什么事呢?它唯一能够知道的机会就是:被加到对象内的那些数据身上附带有某种指示,指示constructor 进行到什么程度。那么destructor就可以检查这些数据并(或许能够)理解应该如何应对。如此繁重的簿记工作会降低constructors的速度,使每一个对象变得更庞大。C++避免这样的额外开销,但你必须付出"仅部分构造完成"的对象不会被自动销毁的代价(条款E13有另一个"效率与程序行为"之间的类似取舍决定)。
由于C++不自动清理那些"构造期间抛出exceptions"的对象,所以你必须设计你的constructors,使它们在那种情况下亦能自我清理。通常这只需将所有可能的exceptions 捕捉起来,执行某种清理工作,然后重新抛出exception,使它继续传播出去即可。这个策略可以这样纳入 BookEntry constructors
cpp
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
theName(name), theAddress(address),
theImage(0), theAudioClip(0)
{
try {
// 这个try 语句块是新的。
if (imageFileName != "")
{
theImage = new Image(imageFileName);
}
if (audioClipFileName != "")
{
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch (...) //捕捉所有的exception。
{
delete theImage; //执行必要的清理工作。
delete theAudioClip;
throw;// 继续传播这个 exception。
}
}
不需要担心 BookEntry 的non-pointer data members。
Data members 会在class constructor 被调用之前就先初始化好(译注:因为此处使用了member initialization list,成员初值链表),所以当BookEntry constructor 函数本体开始执行,该对象的theName,theAddress 和 thePhones 等 data members 都已完全构造好了。
所以当BookEntry object 被销毁,其所内含的这此data members 就像"构造完全的对象"一样,也会被自动销毁,无须你插手。
当然啦,如果这些对象的constructors调用其他函数,而那些函数可能抛出exceptions,那么这些constructors 就必须负责捕捉exceptions,并在继续传播它们之前先执行任何必要的清理工作。
你可能已经注意到,BookEntry的catch 语句块内的动作和BookEntry的destructor内的动作相同。 我们一向不遗余力地希望消除重复代码,这里也是一样的,所以最好是把共享代码抽出放进一个private 辅助函数内,然后让constructor 和destructor 都调用它:
cpp
class BookEntry {
public:
//与前同。
private:
void cleanup()
//共同的清理(clean up)动作放在这里
};
void BookEntry::cleanup()
{
delete theImage;
delete theAudioclip;
}
BookEntry::BookEntry(const string& name,
const strings address,
const string& imageFileName,
const string& audioclipFileName)
:theName(name), theAddress(address),
theImage(0), theAudioclip(0)
{
try {
// 与前同。
}
catch (...)
{
cleanup();//释放资源。
throw;//传播 exception。
}
}
BookEntry::~BookEntry()
{
cleanup()//释放资源。
}
}:
好极了,但是本题并未就此结束。让我们稍加变化,让 theImage和theAudioClip 都变成常量指针:
cpp
class BookEntry
{
public:
//与前同。
private:
//这些指针都是const。
Image*const theImage;
Audioclip* const theAudioclip;
};
这样的指针必须通过 BookEntry constructors 的成员初值链表(member initialization lists)加以初始化,因为再没有其他方法可以给予const 指针一个值(见条款E12)。
一个常见的做法就是像下面这样给予theImage 和 theAudioClip 初值:
cpp
//注意,以下做法在发生 exception 时会导致资源泄漏
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName):
theName(name), theAddress(address),
theImage(imageFilename != ""
?new Image(imageFileName)
:0),
theAudioClip(audioclipFileName != ""
?new Audioclip(audioClipFileName)
:0)
{}
但这却导致我们最初极力想消除的问题:如果在theAudioc1ip初始化期间发生exception, theImage 所指对象并不会被销毁。
此外,我们也无法借此在constructor内加上try/catch 语句块来解决此问题,因为try和catch都是语句(statements),而member initialization lists只接受表达式(expressions)。这就是为什么我们必须使用?:操作符取代 if-then-else 语法来为theImage和theAudioClip设定初值的原因。
尽管如此,欲在"exceptions 传播至constructor 外"之前执行清理工作,唯一的机会就是捕捉那些exceptions.
所以既然我们无法将try和catch放到一个member initialization list 之中,势必得将它们放到其他某处。
一个可能的地点就是放到某些private member functions内,让 theImage 和 theAudioClip 在其中获得初值:
cpp
class BookEntry {
public:
private:
//与前同。
// data members 与前同。
Image* initImage(const strings imageFileName);
AudioClip* initAudioClip(const string& audioClipFileName)
};
BookEntry::BookEntry(const string& name,
const string& address, const string& imageFileName, const string& audioclipFileName)
:theName(name),theAddress(address),
theImage(initImage(imageFileName)),
theAudioClip(initAudioClip(audioClipFileName))
{}
// theImage 首先被初始化,所以即使初始化失败亦无须担心
// 资源泄漏问题。因此本函数不必处理任何exceptions。
Image* BookEntry::initImage(const strings imagerileName)
{
if (imageFileName)
return new Image(imageFileName);
else
return 0;
}
// theAudioClip第二个被初始化,所以如果在它初始化期间有
// exception 被抛出,它必须确定将theImage 的资源释放掉。
//这就是为什么本函数使用try...catch 的原因。
AudioClip* BookEntry::initAudioclip(const string&audioClipFileName)
{
try {
if (audioClipFileName != "")
return new AudioClip(audioClipFileName);
else
return 0;
}
catch (...)
{
delete theImage;
throw;
}
}
这是个完美的结局,它解决了使我们左支右绌疲于奔命的问题。
缺点是:概念上应该由constructor 完成的动作现在却散布于数个函数中,造成维护上的困扰。
**一个更好的解答是,接受条款9的忠告,将theImage和theAudioClip所指对象视为资源,交给局部对象来管理。**这个办法立足所依据的事实是,不论theImage 和theAudioClip都是指向动态分配而得的对象,当指针本身停止活动,那些对象都应该被删除。这正是auto_ptr class(见条款9)的设计目的。所以我们可以将 theImage 和 theAudioclip 的原始指针类型改为 auto_ptr:
cpp
class BookEntry {
public:
// 与前同。
private:
const auto_ptr<Image> theImage;
// 注意,改用
const auto_ptr<Audioclip> theAudioclip;
// auto _ptr对象。
};
这么做便可以让BookEntry constructor在异常出现时免于资源泄漏的恐惧,也让我们得以利用member initialization list 将theImage和theAudioclip初始化:
cpp
BookEntry::BookEntry(const strings name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
:theName(name), theAddress(address),
theImage(imageFileName != ""
? new Image(imageFileName)
:0),
theAudioClip(audioClipFileName !=""
? new Audioclip(audioClipFileName)
:0)
{}
在此设计中,如果 theAudioc1ip 初始化期间有任何 exception 被抛出:theImage已经是完整构造好的对象,所以它会被自动销毁,就像theName,theAddress 和thePhones 一样。
此外,由于 theImage 和 theAudioClip如今都是对象,当其"宿主"BookEntry被销毁,它们亦将被自动销毁。因此不再需要以手动方式删除它们所指的对象。这会大幅简化 BookEntry destructor:
cpp
BookEntry;:~BookEntry()()
//不需要做什么事!
意味你可以完全摆脱 BookEntry destructor。
结论是:如果你以auto_ptr 对象来取代pointer class members,你便对你的constructors 做了强化工事,免除了"exceptions 出现时发生资源泄漏"的危机,不再需要在destructors 内亲自动手释放资源,并允许const member pointers得以和non-const member pointers 有着一样优雅的处理方式。
处理"构造过程中可能发生的exceptions",相当棘手。 但是auto_ptr(以及与auto ptr相似的classes)可以消除大部分劳役。使用它们,不仅能够让代码更容易理解,也使程序在面对exceptions时更健壮。