条款13: 以对象管理资源

结论

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes分别是tr1::share_ptr和auto_ptr,前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使他们指向null。

先通过一段小白的代码案例引入主题。假设我们有一个用来表示投资行为的程序库,其中各式各样的投资类型继承自一个root class Investment:

class Investment {...};		// “投资类型”继承体系中的root class

进一步假设,这个程序库通过一个工厂函数(factory function)供应我们某特定的Investment对象:

Investment* createInvestment();		// 返回指针,指向Investment继承体系内的动态分配对象。
									// 调用者有责任删除它。这里为了简化,刻意不写参数。

一如以上注释所言,createInvestment的调用端使用了函数返回的对象后,有责任删除之。现在考虑有个f函数履行了这个责任:

void f()
{
	Investment* pInv = createInvestment();		// 调用factory函数
	...
	delete pInv;								// 释放pInv所指对象
}

代码缺陷分析

这看起来似乎妥当,但若干情况下f可能无法删除它得自createInvestment的投资对象——1)或许因为“…”区域内的一个过早的return语句。如果这样一个return被执行起来,控制流就绝不会触及delete语句;2)类似情况发生在对createInvestment的使用及delete动作位于某循环内,而该循环由于某个continue或goto语句过早退出;3)最后一种可能是“…”区域内的语句跑出异常,果真如此控制流将再次不会幸临delete。无论delete如何被略过去,我们泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源

代码的可维护性也是需要考虑的一个方面。谨慎地编写程序可以防止这一类错误。但是你必须想,代码可能会在时间渐渐过去后被修改。一旦软件开始接受维护,可能会有某些人添加return语句或者continue语句而未能全然领悟它对函数的资源管理策略造成的后果。更糟糕的是f的“…”区域有可能调用一个“过去从不抛出异常,却在被“改善”之后开始那么做”的函数。因此单纯依赖“f总是会执行delete语句”是行不通的。

解决方案:RAII(Resource Acquisition Is Initialization)

为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,我们便可以依赖C++的“析构函数自动调用机制”确保资源被释放。

方案一:auto_ptr

auto_ptr是个“类指针对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。许多资源被动态分配于heap内而后被用于单一区块或函数内,它们应该是在控制流离开那个区块或函数时被释放。下面示范如何使用auto_ptr以避免f函数潜在的资源泄漏的可能性:

void f()
{
	std::auto_ptr<Investment> pInv(createInvestment());	// 调用factory函数
														// 一如即往地使用pInv
	...													// 经由auto_ptr的析构函数自动删除pInv
}

这个简单的例子示范**“以对象管理资源”的两个关键想法**:

  • 获得资源后立刻放进管理对象内。以上代码中createInvestment返回的资源被当作其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition is Initialization;RAII),因为我们几乎总是在获得一笔资源后同一语句内以它初始化某个管理对象。有时候活的的资源被拿来赋值(而非初始化)某个管理对象,但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
  • 管理对象运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放了。如果资源释放动作可能导致抛出异常,事情变得有点棘手,但是条款8能够解决该问题,在这里不多赘述。

auto_ptr使用的注意事项
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。如果真实那样,对象会被删除一次以上,而那会使你的程序搭上驶向“未定义行为”的快速列车上。为了预防这个问题,auto_ptr有一个不同寻常的性质:若通过copy构造函数或者copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一所有权

std::auto_ptr<Investment> pInv1(createInvestment());		// pInv1指向createInvestment返回物
std::auto_ptr<Investment> pInv2(pInv1);						// 现在PInv2指向对象,pInv1被设为null
pInv1 = pInv2;												// 现在pInv1指向对象,pInv2被设为null

方案二:shared_ptr

auto_ptr的替代方案是“引用计数型智慧指针”(reference-counting smart pointer,RCSP)。所谓RCSP也是个智能指针,持续跟踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。RCSP提供的行为类似垃圾回收,不同的是RCSP无法打破环状引用(例如两个其实已经没被使用的对象互指,因而好像还处在“被使用”状态)。

TR1的tr1::shared_ptr就是个RCSP,所以你可以这么写:

void f()
{
	...
	std::TR1::shared_ptr<Investment> pInv(createInvestment());	// 调用factory函数
	...															// 经由shared_ptr析构函数自动删除ptr
}

这段代码看起来几乎和使用auto_ptr的那个版本相同,但shared_ptr的复制行为正常多了:

void f()
{
	std::shared_ptr<Investment> pInv1(createInvestment());		// pInv1指向createInvestment返回物
	std::shared_ptr<Investment> pInv2(pInv1);					// pInv1和PInv2指向同一个对象
	pInv1 = pInv2;												// 同上,无任何改变
	...
}												// pInv1和pInv2被销毁,它们所指的对象也就被自动销毁

由于tr1::shared_ptr的复制行为“一如预期”,它们可以被用于STL容器,以及其他“auto_ptr之非正统复制行为并不适用”的语境上。

注意
auto_ptr和shared_ptr两者都在其析构函数内做delete,而不是delete[]动作。那意味在动态分配而得的array上使用auto_ptr或shared_ptr是个馊主意,尽管如此,但是能编译通过。

std::auto_ptr<std::string> aps(new std::string[10]);	// 馊主意,会用上错误的delete
std::shared_ptr<int> sp1(new int[1024]);				// 相同问题

你会惊讶地发现,并没有特别针对“C++动态分配数组”而设计的类似的auto_ptr或shared_ptr那样的东西,甚至TR1中也没有。那是因为vector和string几乎总是可以取代动态分配而得的数组。如果你还是认为拥有针对数组而设计的,类似auto_ptr和shared_ptr那样的class较好,看看Boost吧。在那你会很高兴地发现boost::scoped_array和boost::shared_array ,它们都能提供你要的行为。

你可能感兴趣的:(Effective,C++,c++)