副标题#e#
假如你正在开拓一个具有多媒体成果的通讯录措施。这个通讯录除了能存储凡是的文字信息如姓名、地点、电话号码外,还能存储照片和声音(可以给出他们名字的正确发音)。
为了实现这个通信录,你可以这样设计:
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 string& audioClipFileName = "");
~BookEntry();
// 通过这个函数插手电话号码
void addPhoneNumber(const PhoneNumber& number);
...
private:
string theName; // 人的姓名
string theAddress; // 他们的地点
list thePhones; // 他的电话号码
Image *theImage; // 他们的图像
AudioClip *theAudioClip; // 他们的一段声音片断
};
通讯录的每个条目都有姓名数据,所以你需要带有参数的结构函数(拜见条款3),不外其它内容(地点、图像和声音的文件名)都是可选的。留意应该利用链表类(list)存储电话号码,这个类是尺度C++类库(STL)中的一个容器类(container classes)。(拜见Effective C++条款49 和本书条款35)
#p#副标题#e#
编写BookEntry 结构函数和析构函数,有一个简朴的要领是:
BookEntry::BookEntry(const string& name,const string& 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 theAudioClip;
}
结构函数把指针theImage和theAudioClip初始化为空,然后假如其对应的结构函数参数不是空,就让这些指针指向真实的工具。析构函数认真删除这些指针,确保BookEntry工具不会产生资源泄漏。因为C++确保删除空指针是安详的,所以BookEntry的析构函数在删除指针前不需要检测这些指针是否指向了某些工具。
看上去仿佛一切精采,在正常环境下确实不错,可是在非正常环境下(譬喻在有异常产生的环境下)它们恐怕就不会精采了。
请想一下假如BookEntry的结构函数正在执行中,一个异常被抛出,会产生什么环境呢?:
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
一个异常被抛出,可以是因为operator new(拜见条款8)不能给AudioClip分派足够的内存,也可以因为AudioClip的结构函数本身抛出一个异常。岂论什么原因,假如在BookEntry结构函数内抛出异常,这个异常将通报到成立BookEntry工具的处所(在结构函数体的外面。 译者注)。
此刻假设成立theAudioClip工具成立时,一个异常被抛出(并且通报措施节制权到BookEntry结构函数的外面),那么谁来认真删除theImage已经指向的工具呢?谜底显然应该是由BookEntry来做,可是这个想虽然的谜底是错的。BookEntry基础不会被挪用,永远不会。
C++仅仅能删除被完全结构的工具(fully contructed objects), 只有一个工具的结构函数完全运行完毕,这个工具才气被完全地结构。所以假如一个BookEntry工具b做为局部工具成立,如下:
void testBookEntryClass()
{
BookEntry b("Addison-Wesley Publishing Company","One Jacob Way, Reading, MA 01867");
...
}
而且在结构b的进程中,一个异常被抛出,b的析构函数不会被挪用。并且假如你试图采纳主动手段处理惩罚异常环境,即当异常产生时挪用delete,如下所示:
void testBookEntryClass()
{
BookEntry *pb = 0;
try {
pb = new BookEntry("Addison-Wesley Publishing Company","One Jacob Way, Reading, MA 01867");
...
}
catch (...) { // 捕捉所有异常
delete pb; // 删除pb,当抛出异常时
throw; // 通报异常给挪用者
}
delete pb; // 正常删除pb
}
你会发此刻BookEntry结构函数里为Image分派的内存仍旧被丢失了,这是因为假如new操纵没有乐成完成,措施不会对pb举办赋值操纵。假如BookEntry的结构函数抛出一个异常,pb将是一个空值,所以在catch块中删除它除了让你本身感受精采以外没有任何浸染。用乖巧指针(smart pointer)类auto_ptr(拜见条款9)取代raw BookEntry*也不会也什么浸染,因为new操纵乐成完成前,也没有对pb举办赋值操纵。
#p#分页标题#e#
C++拒绝为没有完成结构操纵的工具挪用析构函数是有一些原因的,而不是存心为你制造坚苦。原因是:在许多环境下这么做是没有意义的,甚至是有害的。假如为没有完成结构操纵的工具挪用析构函数,析构函数如何去做呢?仅有的步伐是在每个工具里插手一些字节来指示结构函数执行了几多步?然后让析构函数检测这些字节并判定该执行哪些操纵。这样的记录会减慢析构函数的运行速度,并使得工具的尺寸变大。C++制止了这种开销,可是价钱是不能自动地删除被部门结构的工具。(雷同这种在措施行为与效率这间举办折衷处理惩罚的例子还可以拜见Effective C++条款13)
因为当工具在结构中抛出异常后C++不认真排除工具,所以你必需从头设计你的结构函数以让它们本身排除。常常用的要领是捕捉所有的异常,然后执行一些排除代码,最后再从头抛出异常让它继承转递。如下所示,在BookEntry结构函数中利用这个要领:
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(0), theAudioClip(0)
{
try { // 这try block是新插手的
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch (...) { // 捕捉所有异常
delete theImage; // 完成须要的排除代码
delete theAudioClip;
throw; // 继承通报异常
}
}
不消为BookEntry中的非指针数据成员劳神,在类的结构函数被挪用之前数据成员就被自动地初始化。所以假如BookEntry结构函数体开始执行,工具的theName, theAddress 和 thePhones数据成员已经被完全结构好了。这些数据可以被看做是完全结构的工具,所以它们将被自动释放,不消你参与操纵。虽然假如这些工具的结构函数挪用大概会抛出异常的函数,那么哪些结构函数必需去思量捕捉异常,在答允它们继承通报之前完成必须的排除操纵。
你大概已经留意到BookEntry结构函数的catch块中的语句与在BookEntry的析构函数的语句险些一样。这里的代码反复是绝对不行容忍的,所以最好的要领是把通用代码移入一个私有helper function中,让结构函数与析构函数都挪用它。
class BookEntry {
public:
... // 同上
private:
...
void cleanup(); // 通用排除代码
};
void BookEntry::cleanup()
{
delete theImage;
delete theAudioClip;
}
BookEntry::BookEntry(const string& name,const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(0), theAudioClip(0)
{
try {
... // 同上
}
catch (...) {
cleanup(); // 释放资源
throw; // 通报异常
}
}
BookEntry::~BookEntry()
{
cleanup();
}
这就行了,可是它没有思量到下面这种环境。假设我们略微窜改一下设计,让theImage 和theAudioClip是常量(constant)指针范例:
class BookEntry {
public:
... // 同上
private:
...
Image * const theImage; // 指针此刻是
AudioClip * const theAudioClip; // const范例
};
必需通过BookEntry结构函数的成员初始化表来初始化这样的指针,因为再也没有其它处所可以给const指针赋值。凡是会这样初始化theImage和theAudioClip:
// 一个大概在异常抛出时导致资源泄漏的实现要领
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)
{}
这样做导致我们原先一直想制止的问题从头呈现:假如theAudioClip初始化时一个异常被抛出,theImage所指的工具不会被释放。并且我们不能通过在结构函数中增加try和catch 语句来办理问题,因为try和catch是语句,而成员初始化表仅答允有表达式(这就是为什么我们必需在 theImage 和 theAudioClip的初始化中利用?:以取代if-then-else的原因)。
#p#分页标题#e#
无论如何,在异常通报之前完成排除事情的独一的要领就是捕捉这些异常,所以假如我们不能在成员初始化表中放入try和catch语句,我们把它们移到其它处所。一种大概是在私有成员函数中,用这些函数返回指针,指向初始化过的theImage 和 theAudioClip工具。
class BookEntry {
public:
... // 同上
private:
... // 数据成员同上
Image * initImage(const string& 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 被首先初始化,所以纵然这个初始化失败也
// 不消担忧资源泄漏,这个函数不消举办异常处理惩罚。
Image * BookEntry::initImage(const string& imageFileName)
{
if (imageFileName != "") return new Image(imageFileName);
else return 0;
}
// theAudioClip被第二个初始化, 所以假如在theAudioClip
// 初始化进程中抛出异常,它必需确保theImage的资源被释放。
// 因此这个函数利用try...catch 。
AudioClip * BookEntry::initAudioClip(const string& audioClipFileName)
{
try {
if (audioClipFileName != "") {
return new AudioClip(audioClipFileName);
}
else return 0;
}
catch (...) {
delete theImage;
throw;
}
}
上面的措施简直不错,也办理了令我们头疼不已的问题。不外也有缺点,在原则上应该属于结构函数的代码却分手在几个函数里,这令我们很难维护。
更好的办理要领是回收条款9的发起,把theImage 和 theAudioClip指向的工具做为一个资源,被一些局部工具打点。这个办理要领成立在这样一个事实基本上:theImage 和theAudioClip是两个指针,指向动态分派的工具,因此当指针消失的时候,这些工具应该被删除。auto_ptr类就是基于这个目标而设计的。(拜见条款9)因此我们把theImage 和 theAudioClip raw指针范例改成对应的auto_ptr范例。
class BookEntry {
public:
... // 同上
private:
...
const auto_ptr theImage; // 它们此刻是
const auto_ptr theAudioClip; // auto_ptr工具
};
这样做使得BookEntry的结构函数纵然在存在异常的环境下也能做到不泄漏资源,并且让我们可以或许利用成员初始化表来初始化theImage 和 theAudioClip,如下所示:
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){}
在这里,假如在初始化theAudioClip时抛出异常,theImage已经是一个被完全结构的工具,所以它能被自动删除去,就象theName, theAddress和thePhones一样。并且因为theImage 和 theAudioClip此刻是包括在BookEntry中的工具,当BookEntry被删除时它们能被自动地删除。因此不需要手工删除它们所指向的工具。可以这样简化BookEntry的析构函数:
BookEntry::~BookEntry()
{} // nothing to do!
这暗示你能完全去掉BookEntry的析构函数。
综上所述,假如你用对应的auto_ptr工具替代指针成员变量,就可以防备结构函数在存在异常时产生资源泄漏,你也不消手工在析构函数中释放资源,而且你还能象以前利用非const指针一样利用const指针,给其赋值。
在工具结构中,处理惩罚各类抛出异常的大概,是一个棘手的问题,可是auto_ptr(可能雷同于auto_ptr的类)能化繁为简。它不只把令人欠好领略的代码埋没起来,并且使得措施在面临异常的环境下也能保持正常运行。