所谓资源就是,一旦用了它,将来必须还给系统。最常见的资源是动态分配内存,另外常见的资源还有:文件描述器、互斥锁、图形界面中的字形和笔刷、数据库连接、以及网络socket.
Item 13: 以对象管理资源
该条款背后的思想在于:把资源放进对象内,我们被可以倚赖C++的“析构函数自动调用机制”确保资源被释放。
假设如下类及函数:
class Investment { ... }; //继承体系中的root class
Investment* createInvestment(); //返回指针,指向Investment继承体系内的动态分配对象。调用者有责任删除它
void f(){
Investment* pInv = createInvestment();
...
delete pInv; //释放pInv所指对象
}
乍一看函数f()似乎没有什么不妥的,在退出函数之前我们负责任地执行释放动作。可是难以预料的是,...所省略的这段代码里,可能会抛出异常,甚至是过早地执行return语句从而使控制流将不经过delete语句。虽然谨慎地编写程序可以防止这一类错误,但是我们很难保证在后续的开发中,不同人的修改可以保证这段代码不出现我们担忧的情况。许多资源被动态分配于heap内而后被用于单一区块或函数内,它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的,用它可以避免f函数潜在的资源泄漏可能性:
void f(){
std::auto_ptr<Investment> pInv( createInvestment() );
...
} //经由auto_ptr的析构函数自动删除pInv
这个简单的例子示范“以对象管理资源”的两个关键想法:
(1)获得资源后立刻放进管理对象内。
(2)管理对象运用析构函数确保资源被释放。
由于auto_ptr被销毁时自动删除它所指之物,所以受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。事实上,如果通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr。
另一种智能指针是TR1的tr1::shared_ptr,它持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
Item 14: 在资源管理类中小心coping行为
auto_ptr和shared_ptr只适用于heap_based的资源管理,对于非heap_based的资源,我们可能需要建立自己的资源管理类。条款14、15涉及了设计这种类需要考虑的一些细节问题。首先是复制问题。当一个RAII(Resource Acquisition is Initialization)对象被复制,会发生什么事情?复制RAII对象必须一并复制它所管理的资源,所以资源的coping行为决定RAII对象的coping行为。常见的行为有:
(1)禁止复制。如果复制动作对RAII对象不合理,应该通过声明private copying操作并且不予实现。
(2)对底层资源使用“引用计数法”,即shared_ptr的做法。tr1::shared_ptr允许指定所谓的“删除器”,那是一个函数或函数对象,当引用次数为0时便被调用。
(3)复制底部资源。即所谓的深度复制。
(4)转移底部资源的拥有权,正如auto_ptr的做法。
Item 15: 在资源管理类中提供对原始资源的访问
大部分的API直接处理资源,而不是经过封装的资源管理对象。因此我们需要一个函数可将RAII对象转换为其所内含之原始资源,显式地转换或者隐式转换。
tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式转换,而且也重载了指针聚会操作符(operator->和operator*)。
隐式转换由转换操作符。
Item 16: 成对使用new和delete时要采取相同形式
当你使用new(也就是通过new动态生成一个对象),有两件事情发生。第一,内存被分配出来(通过名为operator new的函数)。第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。delete的问题在于,即将被删除的内存这内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。
当你对着一个指针使用delete,唯一能够让delete知道内存中是不叫 存在一个“数组大小记录”的办法就是,由你来告诉它。如果你使用delete时加上[],delete便认定指针指向一个数组,否则它便认定指针指向单一对象。
Item 17: 以独立语句将new出来的对象置入智能指针
假如有以下函数:
int priority(); //计算优先权
void processWidget( std::tr1::shared_ptr<Widget> pw, int priority ); //根据优先级处理Widget
考虑下面的调用:
processWidget( std:tr1::shared_ptr<Widget>( new Widget ), priority() );
在进入processWidget函数体之前,编译器需要做三件事:
(1)调用priority
(2)执行new Widget
(3)调用tr1::shared_ptr构造函数
关于这三件事的执行顺序,我们能肯定的只有,(2)一定执行于(3)之前,但是(1)可能在(2)前(2)后还是(3)前(3)后执行并不确定,如果编译器决定以这样的顺序执行这三件事:
(2),(1),(3)
可是可是,在执行(1)即调用priority()的时候出异常了!new Widget返回的指针将遗失,也就是,在我们还没来得及把它托付给shared_ptr前,它便泄漏了,那么我们便达不到使用智能指针的初衷了。这便是“以独立语句将new出来的对象置入智能指针”的原因了。这样调用processWidget:
std::tr1::shared_ptr<Widget> pw( new Widget ); //在独立语句内把资源托付给智能指针
processWidget( pw, priority() );
这样就不怕编译器进行可能导致泄漏的重新排列了。