天天看点

3、资源管理

条款 13: 以对象资源管理

在以对象管理资源的理念中,关键的两个想法就是:(1)获得资源后立即放入管理对象内;实际上“以对象管理资源”的观念常被称为“资源取得时机就是初始化时机”(Resource Acquisition Is Initialization; RAII),因为我们几乎总是可以在获得一笔资源后于同一语句内以它初始化某个管理对象,也可能是赋值某个对象。(2)管理对象运用析构函数确保资源被释放;不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会自动调用,于是资源被释放。通常可以由auto_ptr和shared_ptr达到目的,但是二者是不一样的。auto_ptr被销毁时会自动删除它所指向之物,所以一定特别注意别让多个auto_ptr同时指向同一个对象。auto_ptr的不寻常性质:若通过copy构造函数或者copy assignment从操作符复制他们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!

std::auto_ptr<Investment> pInv1(CreatInvestment());  //pInv1指向CreatInvestment的返回值
std::auto_ptr<Investment> pInv2(pInv1);  //现在pInv2指向对象,pInv1被设为null
pInv1 = pInv2;  //现在pInv1指向对象,pInv2被设为null
           

这种诡异的复制行为机上底层条件:“受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,意味着auto_ptr并非是管理动态分配资源的神兵利器。auto_ptr的替代方案就是“引用记数型智慧指针(reference-counting smart pointer, RCSP)”,它能持续最总共有多少个对象指向某笔资源,并在无人指向它的时候自动删除资源。由于auto_ptr和shared_ptr二者的析构函数被做的是delete而不是delete [],那么在动态分配的array中不要使用二者。

请记住:

1、为了防止资源泄漏,请使用RAII(Resource Acquisition Is Initialization)对象,在构造函数中获得资源并在析构函数中释放资源;

2、两个被常用的RAII classes分别是shared_ptr和auto_ptr,前者通常是较佳选择,因为其copy行为比较直观正常;后者copy会使它指向null。

条款 14:在资源管理类中小心copying行为

作为资源管理类,在资源获得时机就是初始化时机。如果RAII对象被赋值或者复制,那就会发生两个资源管理对象指向同一份资源。为了解决这一问题,可以有以下选择:

1)禁止复制。可以将copying行为的操作定义成private;2)采用引用计数法。类似于shared_ptr,保留资源直到最优一个对象被销毁;3)深拷贝。重写拷贝构造函数以及赋值操作符进行深度拷贝;4)转移资源的拥有权。类似于auto_ptr,确保永远只有一个对像拥有该资源,在对象复制的同时对资源的拥有权从被复制物转移到目标物。

请记住:

1、赋值RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定了RAII对象的copying行为;

2、普遍常见的ARII类copying行为是:禁止复制、引用计数。不过其他行为也都可能被实现。

条款 15:在资源管理类中提供对原始资源的访问

资源管理类在防止资源泄漏的有效措施,但是许多API直接涉及资源,那么必须绕过资源管理对象直接访问原始资源(raw resource)。例如:在条款13中提到的使用智能指针如auto_ptr或者shared_ptr保存factory函数如createInvestment的结果:

std::tr1::shared_ptr<Invectment> pInv(createInvestment());
假设希望某个函数能够处理Investment对象,像这样:
int daysHeld(const Investment* pi); //返回投资天数
那么调用的时候:
int days = daysHeld(pInv);  //错误!
这样是通不过编译的,因为daysHeld需要的是Investment*指针,而传进去的参数却是std::tr1::shared_ptr<Invectment>的对象
           

这就需要一个函数蒋RAII对象转换成其所包含的原始资源(本例为Investment*),通常有两种做法:隐式转换和显式转换。tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式转换,它会返回智能指针内部的原始指针。

请记住:

1、APIs往往是要求访问原始资源,所以每个RAII类应该提供一个“取得其所管资源”的方法;

2、对原始资源的访问可能经由显式转换或隐式转换。一般显式转换比较安全,但隐式转换对客户比较方便。

条款 16:成对使用new和delete时要采取相同形式

规则很简单:如果你用new时使用[],那么对应调用delete时也使用[];如果你用new时没有使用[],那么对应调用delete时也不要使用[]。注意typedef时:

typedef std::string AddressLines[4];
std::string* pa1 = new AddressLines;
那么在delete时:
delete[] pa1;  //正确!
delete pa1;   //行为未定义!
           

所以为了避免类似的错误发生,最好尽量不要对数组形式做typedef动作。

请记住:

1、如果你用new时使用[],那么对应调用delete时也使用[];如果你用new时没有使用[],那么对应调用delete时也不要使用[]。

条款 17:以独立语句将newed对象置入智能指针

假设存在以下窗口优先级处理函数processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());第一个实参由两部分组成:执行“new Widget”表达式和调用tr1::shared_ptr构造函数。所以在调用processWidget之前编译器必须创建代码,做以下三件事:调用priority、执行“new Widget”和调用tr1::shared_ptr构造函数。但是执行顺序可能是这样:1)执行“new Widget”;2)调用priority;3)调用tr1::shared_ptr构造函数。如果在priority调用中出现异常,那么new Widget返回的指针将会遗失,智能指针无法对其进行资源释放从而引发资源泄漏。避免这类问题的办法很简单:使用语句分离,分别写出1)创建Widget并将其置入只能指针内;2)把只能指针传给processWidget:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
           

编译器没有对跨语句的各项操作进行重新排列的自由,所以以上方法行得通。

请记住:

1、以独立语句将newed对象置入只能指针内,如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。

继续阅读