第三章 资源管理
本章共有如下5个条款:
条款13 以对象管理资源
条款14 在资源管理类中小心 coping 行为
条款15 在资源管理类中提供对原始资源的访问
条款16 成对使用 new 和 delete 时要采取相同形式
条款17 以独立语句将 newed 对象置入智能指针
条款13 以对象管理资源
以对象管理资源的两个关键想法:
- 获得资源后立刻放进管理对象;
- 管理对象运用析构函数确保资源被释放。
看下面例子
class Investment {
...//投资类型,继承体系中的root class
};
Investment* creatInvestment();//factory函数,返回指针,调用者要删除它
//利用creatAndDeleteInves函数建立和删除
void creatAndDeleteInves()
{
Investment* pInv = creatInvestment();//调用factory函数
...;
delete pInv;//释放pInv所指对象
}
存在的问题:在“…”区域内,可能会提前return、抛出异常等没有执行到 delete pInv,则会造成资源的泄露。
为了保证资源总是得到释放,需要将资源放进对象内,当控制流离开 f ,该对象的析构函数会自动释放那些资源。
改进:利用auto_ptr智能指针,避免creatAndDeleteInves函数潜在的资源泄露可能性。
void creatAndDeleteInves()
{
auto_ptr<Investment> pInv(creatAndDeleteInves());//调用factory函数
...;//一些操作,经由auto_ptr的析构函数自动删除pInv
}
需要注意的是:为了预防 auto_ptr 同时指向一个对象时,被删除多次,auto_ptr有个性质:若通过 copy 构造函数或者 copy assignment 操作符复制它们,它们会变成null,而复制所得的指针获得资源的唯一拥有权。
目前 auto_ptr 的替代方案是 shared_ptr ,通过引用计数来持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除。
void creatAndDeleteInves()
{
...;
shared_ptr<Investment> pInv1(creatAndDeleteInves());//调用factory函数
shared_ptr<Investment> pInv2(pInv1);//利用copy构造函数,pInv1和pInv2指向同一对象
pInv1 = pInv2;//copy assignment操作符,无任何改变
//pInv1和pInv2被销毁
}
更多关于智能指针的内容可以参考这篇文章 C++11中智能指针的原理、使用、实现
条款14 在资源管理类中小心 coping 行为
并非所有资源都是heap-based,例如互斥锁对象Mutex, 则shared_ptr不适用,需要建立自己的资源管理类,在构造函数内获取资源,在析构函数内释放资源。
- 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为;
- 普遍的 RAII class copying 行为是:禁止复制(条款6提到:将 coping 操作声明为 private)、采用 shared_ptr 中的引用计数方法(reference counting)
条款15 在资源管理类中提供对原始资源的访问
条款13提到了通过资源管理类来处理和资源之间的互动,避免直接处理原始资源。但有些 API 会绕过资源管理对象,直接访问原始资源。
shared_ptr<Investment> pInv(createInvestment());
//假设以某个函数处理 Investment 对象
int daysHeld(const Investment* pi);//返回投资天数
下面的调用方式肯定是错误的:
int days = daysHeld(pInv);//错误
因为函数需要的是 Investment* 指针,你传递的是一个类型为shared_ptr<Investment>
的对象。所以需要一个函数将 RAII 对象转换为内含的原始资源。
有两种方法:隐式转换和显式转换。
1)、显式转换
shared_ptr 和 auto_ptr 都提供了一个成员函数 get 返回内部的原始指针,这是显式转换。
int days = daysHeld(pInv.get()); //没有问题
2)、隐式转换
shared_ptr 和 auto_ptr 都重载了操作符 operator->和operator*,这样就允许隐式转换到原始指针。
举例:假设 Investment 类有个成员函数bool isTaxFree() const;
那么下面的调用是OK的
bool taxable1 = !(pInv->isTaxFree()); //经由operator->访问资源
bool taxable2 = !((*pInv).isTaxFree()); //经由operator*访问资源
现在的问题是,需要原始指针的地方(例如,函数形参),如何以智能指针代替?解决方法是:提供一个隐式转换函数。这里不再举例子了。
两条建议:
- APIs 往往要求访问原始资源(raw resources),所以每一个RAII Class应该提供一个“取得其所管理之资源”的办法;
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16 成对使用 new 和 delete 时要采取相同形式
new 的过程:1、分配内存(operator new);2、调用一个或多个构造函数建立对象
delete 过程:1、一个或多个析构函数被调用;2、释放内存(operator delete)
string* str1 = new string;//一个string对象
string* str2 = new string[100];//100个string对象组成的数组
delete str1;
delete [] str2;
记住:如果在 new 表达式中使用[ ],必须在相应的 delete 表达式中也使用[ ];如果你在 new 表达式中不使用[ ],一定不要在相应的 delete 表达式中使用[ ]。
条款17 以独立语句将 newed 对象置入智能指针
以独立语句将 new 出来的对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
processWidget(shared_ptr<Widget>(new Widget), priority());
在调用 processWidget
函数之前,编译器必须创建代码,做三件事情:调用priority
、执行new Widget
、调用shared_ptr
构造函数。但编译器会以怎样的次序完成这些事情呢?可以确定的是new Widget
一定执行于shared_ptr
构造函数被调用之前,但对priority
的调用则可以排在第一或第二或第三执行。
当编译器按这样序列操作时:执行new Widget
—>调用priority
—>调用shared_ptr
构造函数,如果对priority
函数调用异常,则new Widget
返回的指针会遗失,因为它尚未被置入shared_ptr
内。
解决方法:使用分离语句
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
所以,要以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。