《Effective C++:改善程序与设计的55个具体做法》阅读笔记 3——资源管理
3 资源管理
所谓资源就是,一旦用了它,将来必须还给系统。常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中
的字型和笔刷、数据库连接、以及网络sockets。
Item 13: 使用对象管理资源
资源管理对象:资源管理对象管理着其他对象的资源,当资源管理对象的析构函数被调用时,所管理的资源会被自动释放。资源管理对象就是在其析构函数中写了释放所管理对象的资源的语句。
Resource Acquisition Is Initialization资源取得时机便是初始化时机 (RAII):RAII 的基本思想是将资源的生命周期与对象的生命周期绑定在一起。当创建对象时,它获取资源,当对象被销毁时,资源会自动释放。这确保了资源始终得到正确的管理,即使面临异常或其他错误条件。如std::auto_ptr pInv(createInvestment());
,其中createInvestment()
返回资源,并利用此资源初始化pInv
,pInv
管理的就是createInvestment()
所返回的对象的资源。
书中介绍了两种资源管理类,分别为std::auto_ptr
和tr1::shared_ptr
std::auto_ptr
:被拷贝的auto_ptr对象会被置空。
tr1::shared_ptr
:拷贝操作发生时,对象的引用计数加一。
auto_ptr
或tr1::shared_ptr
不能用于动态分配的数组,因为它们背后使用的是delete,而不是delete []。
动态数组可以使用boost::scoped_array
和boost::shared_array
Item 14 在资源管理类中小心coping行为
Item 13的具体实践:使用一个资源管理对象对互斥量进行管理。一般会建立一个区块来使用资源管理对象,只要出了区块资源管理对象就会对互斥量进行解锁操作。
复制RAII对象(资源管理类的对象)必须一并复制它所管理的资源,所以资源的copying 行为决定RAII对象的copying行为。资源管理类中小心coping行为,一般对资源管理类的copy行为的处理方法有:
- 禁止复制。
- 对底层资源祭出“引用计数法”,如使用tr1::shared ptr。当成员变量mutexPtr为tr1::shared ptr类型时,复制操作就会导致引用计数加一。tr1::shared ptr类型对象在引用计数变为零时的默认行为时删除管理的对象,我们还可以自定义tr1::shared ptr类型对象在引用计数变为零时的行为,如下:
std::tr1::shared_ptr<Mutex> mutexPtr(pm, unlock); // pm的引用计算变为零时,调用unlock函数,pm为unlock的形参。当引用计算变为零时,进行解锁,而不是删除pm。
Item 15 在资源管理类中提供对原始资源的访问
获取RAII对象中原始资源:
- 显式转换:tr1::shared_ ptr和auto_ ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件)。或者直接使用
*
。 - 隐式转换:隐式转化可能导致意想不到的问题,比如你不知道怎么程序就运行起来了,或者怎么程序就不能运行了。
对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
类中的隐式转换函数:
#include<iostream>
using namespace std;
class A{
public:
A(int f,char b):f(f),b(b){}
operator char() const // 隐式转换函数,A会隐式转换成char类型的b
{
return b;
}
private:
int f;
char b;
};
int main(){
A x(2,'a');
char y = x;
cout<<y<<endl;
return 0;
}
Item 16 成对使用new和delete时要采取相同形式
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[],如下:
delete stringPtr1; //删除一个对象
delete [] stringPtr2; //删除一个由对象组成的数组
- 使用delete []删除一个对象:未定义的行为,但可以猜测内部发生的事——delete会读取若干内存并将它解释为“数组大小”,然后开始多次调用析构函数,浑然不知它所处理的那块内存不但不是个数组,也或许并未持有它正忙着销毁的那种类型的对象。
- 使用delete删除对象组成的数组:未定义的行为,但可以猜测内部发生的事——可能导致太少的析构函数被调用。
使用typedef要特别注意,一不小心就使用错了detelte的形式,如下:
typedef std::string AddressLines[4];
std::string *pal=new AddressLines;
delete [] pal; // 使用delete[],而不是delete
为避免诸如此类的错误,最好尽量不要对数组形式做typedefs动作。这很容易达成,因为C++标准程序库(条款54)含有string, vector等templates,可将数组的需求降至几乎为零。例如你可以将本例的AddressLines定义为“由strings组成的一个vector”,也就是其类型为vector
typedef vector<string> AddressLines;
Item 17 以独立语句将newed对象置入智能指针
processWidget(std::tr1::shared ptr<Widget>(new widget), priority());
于是在调用processWidget之前,编译器必须创建代码,做以下三件事:
- 调用priority
- 执行new widget
- 调用tr1::shared ptr构造函数
首先new widget肯定是先于tr1::shared ptr构造函数的调用。但是priority何时执行,在C++中是不确定的(java和C#有特定的执行顺序),假设执行顺序如下:
- 执行new widget
- 调用priority
- 调用tr1::shared ptr构造函数
万一对priority的调用导致异常,"new widget"返回的指针将会遗失,因为它尚未被置入tr1::shared ptr内。"new widget"生成的对象未使用tr1::shared ptr进行管理,也未使用delete释放空间,这就会导致内存泄漏。
改进方法:
std::tr1::shared_ptr <Widget> pw(new Widget);
processWidget (pw, priority()) ;
上述得到的启示:将newed对象存储于(置入)智能指针内最好使用一条独立的语句,而不是嵌套在某个语句中。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。