条款10:在constructors 内阻止资源泄漏(resource leak)

想象你正在开发一个多媒体通信簿软件。这个软件可以放置包括人名、地址、电话号码等文字,以及一张个人相片和一段个人声音(或许是其姓名的发音)。

为实现此软件,你可能设计如下:

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时更健壮。

相关推荐
菜鸟学Python1 分钟前
Python 数据分析核心库大全!
开发语言·python·数据挖掘·数据分析
C++忠实粉丝2 分钟前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
一个小坑货8 分钟前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2713 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
古月居GYH13 分钟前
在C++上实现反射用法
java·开发语言·c++
Betty’s Sweet16 分钟前
[C++]:IO流
c++·文件·fstream·sstream·iostream
敲上瘾30 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
不会写代码的ys36 分钟前
【类与对象】--对象之舞,类之华章,共绘C++之美
c++
兵哥工控38 分钟前
MFC工控项目实例三十二模拟量校正值添加修改删除
c++·mfc
在下不上天39 分钟前
Flume日志采集系统的部署,实现flume负载均衡,flume故障恢复
大数据·开发语言·python