20180308 C++ 以对象管理资源‘
假设我们使用一个用来塑模投资行为(如股票等)的程序库,其中各式各样的投资类型继承自一个root class Investment:
class Investment{...};//"投资类型"继承体系中的root class
进一步假设,这个程序库系通过一个工厂函数(factory function)供应我们某特定的Investment对象:
Investment *creatInvestment();//返回指针,指向Investment继承
//体系内的动态分配对象,
//调用者有责任删除它
//这里为了简化,刻意不写参数。
一如以上注释所言,createInvestment的调用端使用了函数返回的对象后,有责任删除之,现在考虑有个f函数履行了这个责任:
void()
{
Investment *pInv = createInvestment();//调用factory函数
...
delete pInv;//释放pInv所指对象
}
这看起来妥当,但若干情况下f可能无法删除得自createInvestment的投资对象--或许因为"..."区域内的一个过早的return语句。若delete被略过去,我们泄露的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。
为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。该方法是把资源放进对象内,这样我们就可以依赖C++的“析构函数自动调用机制”确保资源被释放。
许多资源被动态分配于heap(堆)内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品。auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。下面示范如何使用auto_ptr以避免f函数潜在的资源泄露可能性:
void f()
{
std::auto_ptr<Investmemt> pInv(creatInvestment());
//调用factory函数
//一如以往地使用pInv
//经由auto_ptr的析构函数 自动删除pInv
...
}
这个例子显示了“以对象管理资源”的两个关键想法:
1、“获得资源后立刻放进管理对象(managing object)内”:以上代码中createInvestment返回的资源被当做其管理者auto_ptr的初值。实际上“以对象管理资源”的观点常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被用来赋值(而非初始化)某个管理对象,但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
2、“管理对象(managing object)运用析构函数确保资源被释放”:不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用。于是资源被释放。
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。如果真是那样,对象会被删除一次以上,而那会使你的程序搭上驶向“未定义行为”的快速列车上。为了预防这个问题,auto_ptrs有一个不寻常的性质:若通过拷贝构造函数或拷贝赋值操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。
std::auto_ptr<Investment> pInv1(createInvestment());//pInv1指向
//createInvestment返回物
std::auto_ptr<Investment> pInv2(pInv1);//现在pInv2指向对象
//pInv1被设为null
pInv1 = pInv2;// 现在pInv1指向对象,pInv2被设为null
这一诡异的复制行为,复加上其底层条件:“受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,意味着auto_ptrs并非管理动态分配资源的神兵利器。举个例子,STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr。
auto_ptr的替代方案是“引用计数型智慧指针”(reference-counting smart pointer;RCSP)。所谓RCSP也是个智能指针,持续追踪共有多对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收(garbage collection),不同的是RCSP是无法打破环状引用(cycles of reference,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。
TR1的tr1:shared::ptr就是个RCSP,所以你可以这么写f:
void f()
{
std::tr1::shared_ptr<Investmemt> pInv(creatInvestment());
//调用factory函数
//使用pInv一如以往
//经由auto_ptr的析构函数 自动删除pInv
...
} //经由shared_ptr析构函数自动删除pInv
这段代码看起来几乎和使用auto_ptr的那个版本相同,但shared_ptrs的复制行为正常多了:
void f(){
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment());//pInv1指向
//createInvestment返回物
std::tr1::shared_ptr<Investment> pInv2(pInv1);//pInv和pInv2指向同一个对象
pInv1 = pInv2;//同上,无任何改变
...
} //pInv1和pInv2被销毁,它们所指的对象也就被自动销毁
由于tr1::shared_ptrs的复制行为“一如预期”,它们可被用于STL容器以及其他“auto_ptr的非正统复制行为并不适用”的语境上。
auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。那意味着在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是个馊主意,尽管如此,那么做仍然能通过编译:
std::auto_ptr<std::string> aps(new std::string[10]);//馊主意! 会用上错误的
//delete形式
std::tr1::shared_ptr<int> spi(new int[1024]);//相同问题
并没有特别针对“C++动态分配数组”而设计的类似auto_ptr或tr1::shared_ptr那样的东西,因为vector和string几乎总是可以取代动态分配而得的数组,但在Boost中,boost::scoped_array和boost::shared_array classes可以提供类似的行为。
注意:
1、为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
2、两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是最佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。