【more effective c++读书笔记】【第3章】异常(1)

条款9:利用Destructor避免泄漏资源
1、异常无法被忽略:如果一个函数利用“设定状态变量”的方式或是利用“返回错误码”的方式发出一个异常信号,无法保证此函数的调用者会检查那个变量或检验那个错误码。于是程序的执行可能会一直继续下去,远离错误发生地点。但是如果函数以抛出expcetions的方式发出异常信号,而该exception未被捕捉,程序的执行便会立刻中止。
2、例子:
class ALA{
public:
	virtual void processAdoption() = 0;
	...
};
class Puppy : public ALA{
public:
	virtual void processAdoption();
	...
};
class Kitten : public ALA{
public:
	virtual void processAdoption()
	...
};
void processAdoptions(istream& dataSource){
	while (dataSource){
		ALA *pa = readALA(dataSource);
		pa->processAdoption();
		delete pa;
	}
}
如果pa-> processAdoption ()函数抛出异常,processAdoptions无法捕捉它,这个异常会传播到processAdoptions的调用端。pa->process()之后的语句都会被跳过,不再执行,意味pa不会被删除,从而导致资源泄漏。
第一种解决方法:
void  processAdoptions(istream& dataSource){
	while (dataSource){
		ALA *pa = readALA(dataSource);
		try{
			pa->processAdoption();
		}
		catch (...){
			delete pa;
			throw;
		}
		delete pa;
	}
}
上述函数被try语句和catch语句搞得乱七八糟,更重要的是,被迫重复撰写其实可被正常路线和异常路线共享的清理代码,本例指的是delete动作,这对程序的维护造成困扰。
第二种解决方法:以一个类似指针的对象取代指针pa。当类似指针的对象被销毁时,我们可以令它的析构函数调用delete。
void processAdoptions(istream& dataSource){
	while (dataSource){
		auto_ptr<ALA> pa(readALA(dataSource));
		pa->processAdoption();
	}
}
隐藏在auto_ptr背后的观念——以一个对象存放必须自动释放的资源,并依赖该对象的destructor释放——亦可以以指针为本以外的资源施行。
3、坚持一个规则:把资源封装在对象内,通常便可以在exceptions出现时避免泄漏资源。

条款10:在constructors内阻止资源泄漏
例子:
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();
	....
private:
	string theName;
	string theAddress;
	list<PhoneNumber> thePhones;
	Image* theImage;
	AudioClip* theAudioClip;
};
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;
}
void testBookEntryClass(){
	BookEntry b("Addison-Wesley Publishing Company",
		"One Jacob Way, Reading, MA 01867");
	...
}

当构造函数中第二个new抛出异常时,第一个new的对象会导致内存泄漏。因为C++只会析构已构造完成的对象,而对象只有在其constructor执行完毕时才算完全构造妥当。如果exception在b的构造过程中被抛出,b的析构函数不会被调用。

如果将b分配与heap中,并在exception出现时调用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 object还是泄漏了,因为除非new动作成功,否则上述那个赋值不会施加于pb身上。如果BookEntry构造函数抛出一个异常,pb将成为null指针,在catch块中删除它没有任何作用。

解决方法:

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 (...) {                      // 捕获所有异常
		delete theImage;                 // 完成必要的清除代码
		delete theAudioClip;
		throw;                           // 继续传递异常
	}
}
但如果让theImagetheAudioClip都变成指针常量,即:
class BookEntry{
public:
	.... //与前同
private:
	.... //与前同
	const Image* theImage;
	const 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(audioClipFileName != "")? new AudioClip(audioClipFileName):0)
{}

导致上述问题仍然存在,解决方法:

class BookEntry{
public:
	... //与前同
private:
	... //与前同
	Image* initImage(constr string& imageFileName);
	AudioClip* initAudioClip(constr string& audioClipFileName);
};
BookEntry::BookEntry(const string& name,
	const string& address
	const string& imageFileName,
	const string& audioClipFileName) :
	theName(name), theAddress(address), theImage(0), theAudioClip(0), theImage(initImage(imageFileName)),
	theAudioClip(initAudioClip(audioClipFileName))
{
}
//theImage首先被初始化,即使初始化失败也无须担心资源泄漏问题
Image* BookEntry::initImage(const string& imageFileName){
	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 Image;
		throw;
	}
}

缺点:概念上应该由constructor完成的动作现在却散布于数个函数中,造成维护上的困扰。
更好的解决方法:

class BookEntry{
public:
	.... //与前同
private:
	constr auto_ptr<Image> theImage; //注意,改用auto_ptr对象
	cosntr auto_ptr<AudioClip> theAudioClip;//注意,改用auto_ptr对象
};
//在此设计中,如果theAudioClip初始化期间出现任何异常,theImage都是已构造好的对象,所以它会被自动销毁。
BookEntry::BookEntry(const string& name,
	const string& address
	const string& imageFileName,
	const string& audioClipFileName) :
	theName(name), theAddress(address), theImage(0), theAudioClip(0), theImage(imageFileName != "" ? new Image(imageFileName) : 0),
	theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName)
{}
//由于theImage和theAudioClip如今都是对象,当其“宿主”BookEntry被销毁时,它们亦将被自动销毁,无需手动方式删除它们
BookEntry::~BookEntry() {}


条款11:禁止异常信息(exceptions)传递到析构函数外

1、两种情况下析构函数会被调用。第一种情况是当对象在正常状态下被销毁,也就是当它离开了它的生存空间或是被明确地删除。第二种情况是当对象被异常处理机制——也就是异常传播过程中的stack-unwinding(栈展开)——销毁。

2、如果控制权基于exception的因素离开destructor,而此时正有另一个exception处于作用状态,C++会自动调用terminate函数,将你的程序结束掉,甚至不等局部对象被销毁。

例子:

考虑一个用来监视在线计算的活动--也就是从你登录开始直到退出为止的所有行为的session class。每个session object都会记录其构造和析构的日期和时间。

class Session{
public:
	Session();
	~Session();
private:
	static void logCreation(Session* objAddr);
	static void logDestruction(Session* objAddr);
};
Session::~Session(){
	logDestruction(this);
}

如果logDestruction抛出一个异常,这个异常并不会被Session Destructor捕捉,所以它会传播到destructor的调用端。但是万一这个destructor本身是因其他某个异常被调用的,terminate函数会被自动调用。

解决方法:

Session::~Session(){
	try{ 
		logDestructor(this); 
	}
	catch (...){}
}

这个语句块阻止了logDestructor所抛出的exception传出Session Destructor之外。

3、如果exception从destructor内抛出,而没有在当地被捕获,那个destructor便执行不全(仅执行到抛出exception的哪一点为止)。如果destructor执行不全,就是没有完成它应该完成的每一件事情。

4、阻止异常传出destructor之外的两个好处:a、它可以避免terminate函数在异常传播过程的栈展开机制中被调用;b、它可以协助确保destructors完成其应该完成的所有事情。


版权声明:本文为博主原创文章,未经博主允许不得转载。

posted on 2015-08-28 09:43  ruan875417  阅读(171)  评论(0编辑  收藏  举报

导航