第三章,异常

条款10 在constructor内阻止资源泄露

C++只会析构已经完成构造的对象!

//.h
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 = "");
~BoolEntry();

// 添加电话号码
void addPhoneNumber(const PhoneNumber& number);
...

private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
Image *theImage;
AudioClip *theAudioClip;
}


//.cpp
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;
}

观察上面的代码,似乎是一种理所应当的设计。但是当其constructor未执行完毕时抛出异常,由于对象未构造完毕,C++不会调用该对象的析构函数,这会导致theImage或theAudioClip未被清理释放,导致资源泄露!

针对这种情况,可以想到的最直接方式就是,在constructor内部写try catch语句

BookEntry::BookEntry(const string& name,
const string& address = "",
const string& imageFileName = "",
const string& audioClipFileName = "")
: theName(name), theAddress(address),
theImage(0),theAudioClip(0)
{
try{
if(imageFileName != ""){
theImage = new Image(imageFileName);
}

if(audioClipFileName != ""){
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch(...){
delete theImage;
delete theAudioClip;
throw;
}
}

BookEntry::~BookEntry()
{
delete theImage;
delete theAudioClip;
}

可以注意到,try catch下的清理语句与析构中的相同,让我们继续不遗余力地消除重复代码,将清理语句写到cleanup函数中。。。


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{
if(imageFileName != ""){
theImage = new Image(imageFileName);
}

if(audioClipFileName != ""){
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch(...){
cleanUp();
throw;
}
}

BookEntry::~BookEntry()
{
cleanUp();
}

看起来不错,但是还有一种情况,如果是常量指针呢?

class BookEntry{
public:
...

private:
...
Image * const theImage;
AudioClip * const theAudioClip;
}

这样的指针,只能通过构造函数的成员初值列表加以初始化,因为没有其他办法可以给予const指针一个值,以下是常见的做法:

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(audioFileName != ""
? new AudioClip(audioClipFileName)
: 0)
{

}

这种情况下,将try catch语句写在constructor中是无济于事的,但也不能直接将try catch语句写入初始化列表(member initialization member function)中。因此有效的办法,应该是将try catch写入private function中,再在初始化列表中调用:

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))
{
...
}

//第一个被初始化的指针,不需要try catch,此前没有申请堆上的成员指针,没有资源泄露的顾虑
Image * BookEntry::initImage(const string& imageFileName)
{
if(imageFileName != "") return new Image(imageFileName);
else return 0;
}

AudioClip * BookEntry::initAudioClip(const string& audioClipFileName)
{
try{
if(audioFileName != ""){
return new AudioClip(audioClipFileName);
}
else return 0;
}
catch(...){
delete theImage;
throw;
}
}

以上就是一路延申下来,构造期间避免异常抛出导致资源泄露的措施。现在从整体布局来看,难免让人满意,因为解决这样一个异常泄露的问题,就需要多个零散的函数来维持,差强人意!有没有最简便,最安全的方式?

接收条款9的忠告,将指针对象视为资源,当指针对象本身停止活动,就应该被删除。所以使用auto_ptr这样的指针老管家,是必要的优雅设计

class BookEntry{
public:
...

private:
...
const auto_ptr<Image> theImage;
const auto_ptr<AudioClip> 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(audioFileName != ""
? new AudioClip(audioClipFileName)
: 0)
{

}

如果theAudioClip初始化期间抛出异常,由于theImage已经完成构造,会被auto_ptr指针管家调用自己的析构释放它,防止了资源泄露。同时,当BookEntry对象销毁时,auto_ptr对象也会销毁,因此不必在BookEntry的析构函数中做清理工作。

总结: 构造过程中可能发生的exception相当棘手,但是使用auto_ptr这样的指针管家可以让代码易于理解的同时应对资源泄露问题消除了大部分劳役!