条款13:Use objects to manage resources
假设我们使用一个用来模拟投资行为的程序库,各种投资类型继承自root class Investment:
class Investment { ... }; //“投资类型”继承体系中的root class
进一步假设,该程序库通过一个工厂函数提供某个特定类型的Investment对象:
Investment* createInvestment(); //简单起见,省略参数
调用createInvestment者有责任在使用了函数返回的对象后删除之。假设有个函数f做了这项工作:
void f() {
Investment *pInv = createInvestment();
...
delete pInv;
}
这看起来妥当,但在某些情况下f可能无法删除它得自createInvestment的投资对象——这可能是因为一个过早的return语句;也有可能对createInvestment的使用及delete动作位于某循环内,而该循环由于某个continue或goto语句过早退出;又或者delete之前有异常抛出。无论如何,一旦delete没有执行,我们泄漏的不只是投资对象的内存,还包括投资对象所保存的任何资源。
当然,谨慎地编写程序可以避免此类错误。但你必须考虑到软件接受维护的情况,后来者可能修改了某些语句导致delete不被执行,因此单纯地倚赖f来释放资源是行不通的。
为确保资源被释放,我们需要将资源放入对象内,当对象被销毁时,其析构函数将会自动释放资源。“以对象管理资源”的观念也叫“资源获得即初始化”(Resource Acquisition Is Initialization;RAII),因为我们总在获取资源后用其初始化某个管理对象。
标准库提供了auto_ptr的RAII类来管理资源。auto_ptr是个“类指针对象”,即“智能指针”,下面介绍如何使用auto_ptr来避免f函数潜在的资源泄漏可能性:
void f() {
std::auto_ptr<Investment> pInv(createInvestment());
... //auto_ptr的析构函数将自动删除pInv
}
由于auto_ptr被销毁时将自动删除所指物,所以一定要注意别让多个auto_ptr指向同一个对象,否则对象将会被删除多次,其结果未定义。为了预防这个问题,auto_ptrs有个特殊的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得指针将获取资源的唯一所有权。
std::auto_ptr<Investment>
pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1);
//现在pInv2指向对象,pInv1为null
pInv1 = pInv2;
//现在pInv1指向对象,pInv2为null
这一诡异的复制行为,加上其底层条件:“受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,造成了auto_ptr在资源管理上的局限性。比如,auto_ptr不能用作STL容器的元素,因为STL容器要求其元素有正常的复制行为。
auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;RCSP)。RCSP将统计指向某笔资源的对象数目,当没有对象指向该资源时自动删除它。其行为类似垃圾回收(garbage collection),不同的是RCSP无法打破环状引用。
TR1的tr1::shared_ptr就是个RCSP,我们可以重写f如下:
void f() {
std::tr1::shared_ptr<Investment>
pInv(createInvestment());
...
}
这段代码和使用auto_ptr的版本几乎一模一样,但shared_ptrs的复制行为就正常多了:
void f() {
std::tr1::shared_ptr<Investment>
pInv1(createInvestment());
std::tr1::shared_ptr<Investment>
pInv2(pInv1);
//pInv1和pInv2指向同一对象
pInv1 = pInv2;
//同上
... //pInv1和pInv2被销毁,
//它们所指的对象也被自动销毁。
}
值得注意的是,auto_ptr和tr1::shared_ptr两者都在析构函数中做delete而不是delete[],因此不要在动态数组上使用auto_ptr或tr1::shared_ptr,尽管这么做可以通过编译:
std::auto_ptr<std::string>
aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);
//会用上错误形式的delete
总结:
- 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常用的RAII类分别是auto_ptr和tr1::shared_ptr,后者是较好的选择,因为其有着正常的复制行为。
条款14:Think carefully about copying behavior in resource-managing classes
对于非heap-based资源,auto_ptr和tr1::shared_ptr并不适用。因此有时我们需要建立自己的资源管理类。
假设我们使用C API处理类型为Mutex的互斥器对象,共有lock和unlock两函数可用:
void lock(Mutex* pm); //锁定pm所指的互斥器对象
void unlock(Mutex* pm); //将互斥器解除锁定
为确保将一个被锁住的Mutex解锁,我们需要建立一个class管理互斥器对象。这样的class的基本结构由RAII守则支配,即“资源在构造期间获得,在析构期间释放”:
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm) {
lock(mutexPtr); //获得资源
}
~Lock() {
unlock(mutexPtr); //释放资源
}
private:
Mutex *mutexPtr;
};
客户对Lock的用法符合RAII方式:
Mutex m; //定义互斥器
...
{ //建立区块定义临界区
Lock ml(&m); //锁定互斥器
... //执行临界区内的操作
} //自动解除互斥器锁定
这很好,但如果Lock对象被复制,会发生什么事?
Lock ml1(&m);
Lock ml2(ml1);
一个一般化的问题时:当一个RAII对象被复制,会发生什么事?通常,有以下几种可能:
- 禁止复制。很多时候允许RAII对象被复制并不合理。这时候,你便应该禁止copying行为。
- 对底层资源进行引用计数。有时候我们希望保存资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该增加其被引用数。这正是tr1::shared_ptr的行为。
通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可实现出reference-counting copying行为。
class Lock { public: explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) { //指定unlock函数为删除器 lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; };
这里不再声明析构函数,而是倚赖编译器的默认行为。
- 复制底层资源
- 转移底层资源的拥有权。就像auto_ptr表现的那样。
条款15:Provide access to raw resources in resource-managing classes
资源管理类很棒,它们是你对抗资源泄漏的堡垒。但很多APIs需要直接访问原始资源,一个设计良好的资源管理类必须能满足这种需求。
有两种做法可以达成目标:显式转换和隐式转换。
tr1::shared_ptr和auto_ptr都提供了一个get成员函数,用来执行显式转换。同时它们也重载了operator->和operator*,允许隐式转换成原始指针。
由于有时候还是必须取得RAII对象内的原始资源,某些RAII class设计者为RAII class提供一个隐式转换函数。考虑下面这个用于字体的RAII class(对C API而言字体是一种原生数据结构):
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font {
public:
explicit Font(FontHandle fh) : f(fh) {}
~Font() { releaseFont(f); }
private:
FontHandle f;
};
假设有大量与字体相关的C API,它们处理的是FontHandles,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。Font class可为此提供一个显式转换函数,像get那样:
class Font {
public:
...
FontHandle get() const { return f; } //显式转换函数
...
};
不幸的是这使得客户每当想要使用API时就必须调用get。
另一个办法是令Font提供隐式转换函数,转型为FontHandle:
class Font {
public:
...
operator FontHandle() const { //隐式转换函数
return f;
}
...
};
这使得调用C API时比较轻松且自然,但是会增加错误发生机会。
总结:
- APIs往往要求访问原始资源(raw resources),所以每个RAII class应该提供一个“取得其所管理的资源”的办法。
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言,显式转换比较安全,隐式转换对客户比较方便。
条款16:Use the same form in corresponding uses of new and delete
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
条款17:Store newed objects in smart pointers in standalone statements
假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
出于对“以对象管理资源”策略的实践,processWidget决定对其动态分配得来的Widget运用智能指针。现在考虑以下调用:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
令人惊讶的是,尽管我们在此使用了“对象管理式资源”,上述调用仍可能造成资源泄漏。
这是因为C++没有规定表达式内的参数计算次序,因此尽管new Widget表达式的执行总是发生在tr1::shared_ptr构造函数之前,但调用priority的动作可能在最前面,中间或最后面。假设priority调用发生在两者之间,而且其调用导致异常,那么new Widget返回的指针将会遗失。是的,在对processWidget的调用过程中可能引发资源泄漏,因为在“资源被创建”和“资源被转换为资源管理对象”的过程中有可能发生异常干扰。
避免此类问题的方法很简单:使用分离语句。
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
以上方法可行,因为编译器对于“跨越语句的各项操作”没有重新排列的自由。
总结:
以独立语句将newed对象存储于智能指针内,否则,一旦抛出异常,有可能导致难以觉察的资源泄漏。