当你考虑到异常、函数内多重回传路径、程序维护员改动软件却没能充分理解随之而来的冲击,那么资源管理就显得复杂的多。
条款13:以对象管理资源
本条款建议程序员使用对象管理资源(如申请的内存),给出的经验是:
(1) 为防止资源泄漏,请使用RAII(“资源取得时机便是初始化时机” (Resource Acquisition Is Initialization; RAII))对象,它们在构造函数中获得资源并在析构函数中释放资源。
(2) 两个常被使用的RAII classes 分别是trl: : shared_ptr 和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
例:
void f()
{
Investment *pInv = createInvestment();
... //这里存在诸多“不定因素”,可能造成delete pInv;得不到执行,这可能就存在潜在的内存泄露。
delete pInv;
}
解决方法:把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。
许多资源被动态分配于堆内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
“以对象管理资源”的两个关键想法:
- 获得资源后立刻放进管理对象内(如auto_ptr)。每一笔资源都在获得的同时立刻被放进管理对象中。“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。
- 管理对象运用析构函数确保资源被释放。即一旦对象被销毁,其析构函数被自动调用来释放资源。
看下面例子:
看下面例子:
std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::auto_ptr<Investment> pInv2(pInv1); //现在pInv2指向对象,而pInv1被设为NULL;
pInv1 = pInv2; //现在pInv1指向对象,而pIn2被设为NULL;
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“有你没我,有我没你”。
auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;SCSP)、它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //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是个潜在危险,资源得不到释放。也许boost::scoped_array和boost::shared_array能提供帮助。还有,vector和string几乎总是可以取代动态分配而得的数组。
请记住:
- 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。
条款14: 在资源管理类中小心 copying 行为
本条款提醒程序员,使用资源管理类时需根据实际需要管理copying行为,常见的有:抑制copying、施行引用计数法。
条款15: 在资源管理类中提供对原始资源的访问
(1) APIs往往要求访问原始资源( raw resources) ,所以每一个RAII class 应该提供一个”取得其所管理之资源”的办法。
(2) 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换(如调用get()函数)比较安全,但隐式转换对客户比较方便。
条款16: 成对使用new 和delete 时要采取相同形式
本条款给出了程序员在申请和释放资源时常犯的错误,给出的经验是:
如果你在new 表达式中使用[],必须在相应的delete表达式中也使用[];如果你在new 表达式中不使用[],一定不要在相应的delete表达式中使用[]。
条款17: 以独立语句将newed 对象置入智能指针
本条款指出了一个使用智能指针时常犯的错误,避免该错误可以这样做:
以独立语句将newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。举例:
processWidget(std::trl::sharedptr<W工dget> (new Widget) , priority());
在调用processWidget之前,编译器必须创建代码,做以下三件事:
(1) 调用priority
(2) 执行”newWidget”
(3) 调用trl:: shared_ptr 构造函数
不同的C++ 编译器执行这三条语句的顺序不一样,但对priority的调用可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行且priority函数抛出了异常,则新创建的对象Widget将导致内存泄漏,解决方法如下:
std::trl::shared_ptr<Widget>pw(new Widget); //在独立语句内以智能指针存储Widget对象
processWidget(pw,priority()); //这个调用肯定不存在内存泄漏