结论:
- 为防止资源泄漏,请使用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语句”是行不通的。
为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,我们便可以依赖C++的“析构函数自动调用机制”确保资源被释放。
auto_ptr是个“类指针对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。许多资源被动态分配于heap内而后被用于单一区块或函数内,它们应该是在控制流离开那个区块或函数时被释放。下面示范如何使用auto_ptr以避免f函数潜在的资源泄漏的可能性:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // 调用factory函数
// 一如即往地使用pInv
... // 经由auto_ptr的析构函数自动删除pInv
}
这个简单的例子示范**“以对象管理资源”的两个关键想法**:
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
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 ,它们都能提供你要的行为。