RAII(Resource acquisition is initialization)
资源获取即初始化
,
它是一项很简单的技术
,
利用
C++
对象生命周期的概念来控制程序的资源
,
例如内存
,
文件句柄
,
网络连接以及审计追踪
(audit trail)
等
.RAII
的基本技术原理很简单
.
若希望保持对某个重要资源的跟踪
,
那么创建一个对象
,
并将资源的生命周期和对象的生命周期相关联
.
如此一来
,
就可以利用
C++
复杂老练的对象管理设施来管理资源
.
最简单的
RAII
形式是
,
创建这样一个对象
:
其构造函数获取一份资源
,
而析构函数则释放这份资源
:
Class Resource {……};
Class ResourceHandle {
Public:
Explicit Resourcehandle (Resource * aResource ):r_( aResource ) //
获取资源
~ ResourceHandle() {delete r_;} //
释放资源
Resource *get() {return r_ ;} //
访问资源
Private:
ResourceHandle (const ResourceHandle &);
ResourceHandle & operator = (const ResourceHandle &);
Resource * r_;
}
ResourceHandle
对象的最美妙之处在于
,
若它被声明为一个函数的局部变量
,
或作为函数的参数
,
或是一个静态对象
,
我们都可以保证析构函数会得到调用
,
从而释放对象所引用的资源
.
当面对草率的维护或传播的异常时
,
若希望保持对某项重要资源的跟踪
,
此为一个可利用的重要的属性
.
考虑以下未使用
RAII
的简单代码
:
Void f ( )
{
Resource *rh = new Resource;
//…
If( iFeelLikeIt () ) //
糟糕的维护
Return;
//…
g(); //
抛出异常
?
Delete rh; //
一定能执行到这里吗
?
}
也许这个函数最初的版本是异常安全的
,rh
所引用的资源总是可以得到释放
.
然而
,
随着时间的推移
,
当一些欠缺谨严的维护人员往代码里添加一些可能回提前返回代码,调用可能回抛出异常的函数
,
或者插入其他一些东西从而使得函数末尾的资源释放代码得不到执行的时候
,
这样的代码就不再是异常安全的了
.
使用
RAII,
可以使函数变得更简单
,
并且更强健
:
Void f ( )
{
ResourceHandle rh ( new Resource );
//…
If( iFeelLikeIt() ) //
没有问题
Return ;
//…
g ( ); //
抛出异常吗
?
没有影响
//rh
析构函数执行
delete
操作
}
当使用
RAII
时
,
只有一种情况无法确保析构函数得到调用
,
就是当
ResourceHandle
对象被分配到堆上时
,
因为这样一来
,
只有显示地
delete
该
ResourceHandle
对象
,
此
ResourceHandle
对象所包含的对象的析构函数才回得到调用
(
其实还有一些边缘性情形
,
着包括调用
abort
或
exit
的情形
,
以及若抛出的异常从未捕获而导致不确定的情形
.):
ResourceHandle * rhp = new ResourceHandle( new Resource ); //
糟糕的注意
!
RAII
技术在
C++
编程中影响是如此深远
,
以至于很难发现有哪个库组件或大型代码未以某种风格使用它们
.
注意
:
可以通过
RAII
进行控制的资源
,
范围非常广泛
.
除了控制本质上以为内存的资源
(
包括缓冲区
,
字符串
,
容器实现等诸如此类的东西
)
外
,
还可以使用
RAII
来控制系统资源
,
包括文件句柄
,
信号量
(semaphore)
以及网络连接等
,
另外还包括一些其他的东西
,
例如登陆会话
(login session),
绘图形状等等
.
考虑下面的类
:
Class Trace
{
Public:
Trace( const char *msg ) : msg_(msg)
{ std::cout << “Entering ” << msg_ <<std::endl ;}
~Trace()
{ std::cout << “leaving ” << msg_ <<std::endl ;}
Private:
Std::string msg_;
};
在
Trace
这个例子中
,
控制的资源是一个准备打印的消息
(
当离开一个作用区域时
).
通过在不同类型的控制流程下监视起生命期来观察不同的
Trace
对象
(
包括自动的
,
静态的
,
局部的以及全局的等等
)
的行为
,
很有教育意义
.
Void f ( )
{
Trace trace (“f”); //
打印
”Entering”
消息
ResourceHandle rh (new Resource ); //
获取资源
//…
If(iFeelLikeIt () )
Return ;
//…
g();
//rh
析构函数执行
delete
操作
//Tracer
析构函数打印
”Leaving”
消息
}
以上代码还展示了关于构造函数和析构函数结构激活的一个重要的定式
:
这些激活形成一个栈
,
确切地说
,
先于
rh
声明并初始化
tracer,
这样就保证
rh
将于
tracer
之前被销毁
(
即后初始化的先销毁
).
推而广之
,
无论何时我们声明了一系列的对象
,
这些对象在运行期将会以特定的顺序被初始化
,
并且最终以相反的顺序被销毁
.
这个析构顺序不会发生变化
,
即使碰到了意外的
return,
传播的异常
,
不寻常的
switch
甚至
goto,
均是如此
.
这个性质对于资源的获取和释放尤其重要
,
因为通常资源必须以特定的顺序进行获取且以相反的顺序进行释放
.
比如
,
在发送一个审计消息之前
,
必须打开一个网络连接
,
而在该网络连接被关闭之前
,
必须发出一个关闭审计消息
(closing audit message).
这种基于栈的行为甚至延伸到了个体对象的初始化和析构方面
.
一个对象的析构函数按照其基类在继承列表中声明的顺序来初始化各个数据成员
,
然后执行构造函数的本体
.