使用析构函数防止资源泄漏

定义一个类

class ALA {
public:
	virtual void processAdoption() ;
... };

函数循环遍历 dataSource 内的信息,处理它所遇到的每个项目。唯一要记住的一 点是在每次循环结尾处删除 pa。这是必须的,因为每次调用 readALA 都建立一个堆对象。 如果不删除对象,循环将产生资源泄漏。

void processAdoptions(istream& dataSource)
{
	while (dataSource) {
	ALA *pa = readALA(dataSource); 
	pa->processAdoption();
	delete pa;
} }

现在考虑一下,如果 pa->processAdoption 抛出了一个异常,将会发生什么? processAdoptions 没有捕获异常,所以异常将传递给 processAdoptions 的调用者。传递中,processAdoptions 函数中的调用 pa->processAdoption 语句后的所有语句都被跳过,这就是说 pa 没有被删除。结果,任何时候 pa->processAdoption 抛出一个异常都会导致 processAdoptions 内存泄漏。

堵塞泄漏很容易 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void processAdoptions(istream& dataSource) {
	while (dataSource) {
		ALA *pa = readALA(dataSource);
 		try {
   		   pa->processAdoption();
		}
		catch (...) { // 捕获所有异常
			delete pa;
   			throw;
		} 
	}
	delete pa;
}

但是你必须用 try 和 catch 对你的代码进行小改动。更重要的是你必须写双份清除代码, 一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须写两个 delete 代码。

我们可以把总被执行的清除代码放入 processAdoptions 函数内的局部对象的析构函数 里,这样可以避免重复书写清除代码。因为当函数返回时局部对象总是被释放,无论函数是 如何退出的。

具体的方法是使用智能指针.使用 shared_ptr 对象代替 raw 指针,processAdoptions 如下所示:

void processAdoptions(istream& dataSource)
{
	while (dataSource) {
	shared_ptr<ALA> pa(readALA(dataSource)); 
	pa->processAdoption();
	} 
}

这个版本的 processAdoptions 在两个方面区别于原来的 processAdoptions 函数。

  1. pa 被声明为一个 shared_ptr对象,而不是一个 raw ALA*指针。
  2. 在循环的结尾没 有 delete 语句。其余部分都一样,因为除了析构的方式,auto_ptr 对象的行为就象一个普 通的指针。是不是很容易。

下面所示的是 shared_ptr 类的一些重要的部分:

1
2
3
4
5
6
7
8
template<class T>
class auto_ptr {
public:
  	auto_ptr(T *p = 0): ptr(p) {}
	~auto_ptr() { delete ptr; } 
private:
	T *ptr; 
};

使用构造函数防止资源泄漏

编写 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 的构造函数正在执行中,一个异常被抛出,会发生什么情况 呢?

if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}

一个异常被抛出,可以是因为operator new不能给 AudioClip 分配足够的内存,也可以因为 AudioClip 的构造函数自己抛出一个异常。不论什么原因,如果在 BookEntry 构造函数内抛出异常,这个异常将传递到建立 BookEntry 对象的地方.

现在假设建立 theAudioClip 对象建立时,一个异常被抛出(而且传递程序控制权到 BookEntry 构造函数的外面),那么谁来负责删除 theImage 已经指向的对象呢?~BookEntry()根本不会被调用.

C++仅仅能删除被完全构造的对象(fully contructed objects), 只有一个对象的构 造函数完全运行完毕,这个对象才被完全地构造。所以如果一个BookEntry对象b做为局部 对象建立,如下:

void testBookEntryClass()
{
	BookEntry b("Addison-Wesley Publishing Company",
					"One Jacob Way, Reading, MA 01867");
... 
}

并且在构造 b 的过程中,一个异常被抛出,b 的析构函数不会被调用。而且如果你试图 采取主动手段处理异常情况,即当异常发生时调用 delete,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void testBookEntryClass()
{
  	BookEntry *pb = 0;
  	try {
		pb = new BookEntry("Addison-Wesley Publishing Company", "One 								Jacob Way, Reading, MA 01867");
	... 
	}
	catch (...) {
   		delete pb;
  	 	throw;
  	}
	delete pb;
}

你会发现在 BookEntry 构造函数里为 Image 分配的内存仍旧被丢失了,这是因为如果 new 操作没有成功完成,程序不会对 pb 进行赋值操作。如果 BookEntry 的构造函数抛出一个异常,pb 将是一个空值,所以在 catch 块中删除它除了让你自己感觉良好以外没有任何作用。

因为当对象在构造中抛出异常后 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 数据成员已经被完全构造好了。这些数据可以被看做是完全构造的对象,所以它们将被自动释放,不用你介入操作。当然如果这些对象的构造函数调用可能会抛出异常的函数,那么那些构造函数必须自己去考虑捕获异常并在继续传递这些异常之前完成必需的清除操作。

假设我们略微改动一下设计,让 theImage 和 theAudioClip 是常量指针类型:

class BookEntry {
public:
... 
private:
...
Image * const theImage; AudioClip * const theAudioClip;
};

必须通过 BookEntry 构造函数的成员初始化表来初始化这样的指针,因为再也没有其它 地方可以给 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(audioClipFileName != ""? new AudioClip(audioClipFileName) : 0) 
{}

这样做导致我们原先一直想避免的问题重新出现:如果 theAudioClip 初始化时一个异 常被抛出,theImage 所指的对象不会被释放。而且我们不能通过在构造函数中增加 try 和catch 语句来解决问题,因为 try 和 catch 是语句,而成员初始化表仅允许有表达式(这也是为什么我们必须在 theImage 和 theAudioClip 的初始化中使用?:以代替 if-then-else 的原因)。

我们把 theImage 和 theAudioClip raw 指针类型改成对应的 shared_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 已经是一个被完全构造的 对象,所以它能被自动删除掉,而且因为 theImage 和 theAudioClip 现在是包含在 BookEntry 中的对象,当 BookEntry 被删除时它们能被自动地删除。因此不需要手工删除它们所指向的对象。可以这样简化BookEntry 的析构函数:

综上所述,如果你用对应的 shared_ptr 对象替代指针成员变量,就可以防止构造函数在 存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非 const 指针一样使用 const 指针,给其赋值。