【1】什么是RAII惯用法?

RAII是Resource Acquisition Is Initialization的缩写,意为“资源获取即初始化”。

它是C++之父Bjarne Stroustrup提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。

软件开发中,会用到各种各样的资源。狭义的资源指内存,而广义的资源包括文件、网络连接、数据库连接、信号量、事件、线程、内存等,甚至可以是状态。

资源获取后由于种种原因导致永久不能释放的资源称为资源泄漏。

针对资源泄漏,提出了各种各样的软件机制和程序设计惯用法,如垃圾收集、RRID[1]、RAII、确定性资源清理等。

RAII 是C++语言的一种管理资源、避免泄漏的惯用法。

C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。

简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

【2】RAII惯用法详解

我记得第一次学到RAII惯用法是在Bjarne Stroustrup的《C++程序设计语言(第3版)》一书中。当讲述C++资源管理时,Bjarne这样写道:

使用局部对象管理资源的技术通常称为“资源获取就是初始化”。这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用。

Bjarne这段话是什么意思呢?

首先让我们来明确资源的概念,在计算机系统中,资源是数量有限且对系统正常运转具有一定作用的元素。

比如,内存,文件句柄,网络套接字(network sockets),互斥锁(mutex locks)等等,它们都属于系统资源。

由于资源的数量不是无限的,有的资源甚至在整个系统中仅有一份,因此我们在使用资源时必须严格遵循的步骤是:

1>获取资源

2>使用资源

3>释放资源

例如 在下面的UseFile函数中:

 void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 在此处使用文件句柄f... // 使用资源
fclose(f); // 释放资源
}

调用fopen()打开文件就是获取文件句柄资源,操作完成之后,调用fclose()关闭文件就是释放该资源。

资源的释放工作至关重要,如果只获取而不释放,那么资源最终会被耗尽。

上面的代码是否能够保证在任何情况下都调用fclose函数呢?请考虑如下情况:

 void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 使用资源
if (!g()) return; // 如果操作g失败!
// ...
if (!h()) return; // 如果操作h失败!
// ...
fclose(f); // 释放资源
}

在使用文件f的过程中,因某些操作失败而造成函数提前返回的现象经常出现。这时函数UseFile的执行流程将变为:

很明显,这里忘记了一个重要的步骤:在操作g或h失败之后,UseFile函数必须首先调用fclose()关闭文件,然后才能返回其调用者,否则会造成资源泄漏。

因此,需要将UseFile函数修改为:

 void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 使用资源
if (!g()) { fclose(f); return; }
// ...
if (!h()) { fclose(f); return; }
// ...
fclose(f); // 释放资源
}

现在的问题是:用于释放资源的代码fclose(f)需要在不同的位置重复书写多次。如果再加入异常处理,情况会变得更加复杂。

例如,在文件f的使用过程中,程序可能会抛出异常:

 void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 使用资源
try {
if (!g()) { fclose(f); return; }
// ...
if (!h()) { fclose(f); return; }
// ...
}
catch (...) {
fclose(f); // 释放资源
throw;
}
fclose(f); // 释放资源
}

我们必须依靠catch(...)来捕获所有的异常,关闭文件f,并重新抛出该异常。随着控制流程复杂度的增加,需要添加资源释放代码的位置会越来越多。

如果资源的数量还不止一个,那么程序员就更加难于招架了。可以想象这种做法的后果是:代码臃肿,效率下降,更重要的是,程序的可理解性和可维护性明显降低。

是否存在一种方法可以实现资源管理的自动化呢?答案是肯定的。

假设UseResources函数要用到n个资源,则进行资源管理的一般模式为:

 void UseResources()
{
// 获取资源1
// ...
// 获取资源n // 使用这些资源 // 释放资源n
// ...
// 释放资源1
}

不难看出资源管理技术的关键在于:要保证资源的释放顺序与获取顺序严格相反。这自然使我们联想到局部对象的创建和销毁过程。

在C++中,定义在栈空间上的局部对象称为自动存储(automatic memory)对象。管理局部对象的任务非常简单,因为它们的创建和销毁工作是由系统自动完成的。

我们只需在某个作用域(scope)中定义局部对象(这时系统自动调用构造函数以创建对象),然后就可以放心大胆地使用之,而不必担心有关善后工作;

当控制流程超出这个作用域的范围时,系统会自动调用析构函数,从而销毁该对象。

读者可能会说:如果系统中的资源也具有如同局部对象一样的特性,自动获取,自动释放,那该有多么美妙啊!。

事实上,您的想法已经与RAII不谋而合了。

既然类是C++中的主要抽象工具,那么就将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务。

这就是RAII惯用法的真谛!可以毫不夸张地说,RAII有效地实现了C++资源管理的自动化。

例如,我们可以将文件句柄FILE抽象为FileHandle类:

 class FileHandle {
public:
FileHandle(char const* n, char const* a) { p = fopen(n, a); }
~FileHandle() { fclose(p); }
private:
// 禁止拷贝操作
FileHandle(FileHandle const&);
FileHandle& operator= (FileHandle const&);
FILE *p;
};

FileHandle类的构造函数调用fopen()获取资源;FileHandle类的析构函数调用fclose()释放资源。

请注意,考虑到FileHandle对象代表一种资源,它并不具有拷贝语义,因此我们将拷贝构造函数和赋值运算符声明为私有成员。

如果利用FileHandle类的局部对象表示文件句柄资源,那么前面的UseFile函数便可简化为:

 void UseFile(char const* fn)
{
FileHandle file(fn, "r");
// 在此处使用文件句柄f...
// 超出此作用域时,系统会自动调用file的析构函数,从而释放资源
}

现在我们就不必担心隐藏在代码之中的return语句了;不管函数是正常结束,还是提前返回,系统都必须“乖乖地”调用f的析构函数,资源一定能被释放。

Bjarne所谓“使用局部对象管理资源的技术……依赖于构造函数和析构函数的性质”,说的正是这种情形。

且慢!如若使用文件file的代码中有异常抛出,难道析构函数还会被调用吗?此时RAII还能如此奏效吗?

问得好。事实上,当一个异常抛出之后,系统沿着函数调用栈,向上寻找catch子句的过程,称为栈辗转开解(stack unwinding)。

C++标准规定,在辗转开解函数调用栈的过程中,系统必须确保调用所有已创建起来的局部对象的析构函数。例如:

 void Foo()
{
FileHandle file1("n1.txt", "r");
FileHandle file2("n2.txt", "w");
Bar(); // 可能抛出异常
FileHandle file3("n3.txt", "rw")
}

当Foo()调用Bar()时,局部对象file1和file2已经在Foo的函数调用栈中创建完毕,而file3却尚未创建。

如果Bar()抛出异常,那么file2和file1的析构函数会被先后调用(注意:析构函数的调用顺序与构造函数相反);

由于此时栈中尚不存在file3对象,因此它的析构函数不会被调用。只有当一个对象的构造函数执行完毕之后,我们才认为该对象的创建工作已经完成。

栈辗转开解过程仅调用那些已创建对象的析构函数。

RAII惯用法同样适用于需要管理多个资源的复杂对象。例如,Widget类的构造函数要获取两个资源:文件myFile和互斥锁myLock。每个资源的获取都有可能失败并且抛出异常。

为了正常使用Widget对象,这里我们必须维护一个不变式(invariant):当调用构造函数时,要么两个资源全都获得,对象创建成功;要么两个资源都没得到,对象创建失败。

获取了文件而没有得到互斥锁的情况永远不能出现,也就是说,不允许建立Widget对象的“半成品”。如果将RAII惯用法应用于成员对象,那么我们就可以实现这个不变式:

 class Widget {
public:
Widget(char const* myFile, char const* myLock)
: file_(myFile), // 获取文件myFile
lock_(myLock) // 获取互斥锁myLock
{}
// ...
private:
FileHandle file_;
LockHandle lock_;
};

FileHandle和LockHandle类的对象作为Widget类的数据成员,分别表示需要获取的文件和互斥锁。资源的获取过程就是两个成员对象的初始化过程。

在此系统会自动地为我们进行资源管理,程序员不必显式地添加任何异常处理代码。

例如,当已经创建完file_,但尚未创建完lock_时,有一个异常被抛出,则系统会调用file_的析构函数,而不会调用lock_的析构函数。

Bjarne所谓构造函数和析构函数“与异常处理的交互作用”,说的就是这种情形。

【3】RAII总结

综上所述,RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终

有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。由此可见,RAII惯用法是进行资源管理的有力武器。

C++程序员依靠RAII写出的代码不仅简洁优雅,而且做到了异常安全。

难怪微软的MSDN杂志在最近的一篇文章中承认:“若论资源管理,谁也比不过标准C++”。

备注:以上【2】和【3】部分转自《RAII惯用法:C++资源管理的利器

Good Good Study, Day Day Up.

顺序  选择  循环  总结

RAII惯用法详解的更多相关文章

  1. SLAM入门之视觉里程计(6):相机标定 张正友经典标定法详解

    想要从二维图像中获取到场景的三维信息,相机的内参数是必须的,在SLAM中,相机通常是提前标定好的.张正友于1998年在论文:"A Flexible New Technique fro Cam ...

  2. KKT条件和拉格朗日乘子法详解

    \(\frac{以梦为马}{晨凫追风}\) 最优化问题的最优性条件,最优化问题的解的必要条件和充分条件 无约束问题的解的必要条件 \(f(x)\)在\(x\)处的梯度向量是0 有约束问题的最优性条件 ...

  3. HS 光流法详解

    前言 本文较为详细地介绍了一种经典的光流法 - HS 光流法. 光流法简介 当人的眼睛与被观察物体发生相对运动时,物体的影像在视网膜平面上形成一系列连续变化的图像,这一系列变化的图像信息不断 &quo ...

  4. 扩展欧几里得算法详解(exgcd)

    一.前言 本博客适合已经学会欧几里得算法的人食用~~~ 二.扩展欧几里得算法 为了更好的理解扩展欧几里得算法,首先你要知道一个叫做贝祖定理的玄学定理: 即如果a.b是整数,那么一定存在整数x.y使得$ ...

  5. 光流法详解之一(LK光流)

    Lucas–Kanade光流算法是一种两帧差分的光流估计算法.它由Bruce D. Lucas 和 Takeo Kanade提出 [1]. LK光流法有三个假设条件: 1. 亮度恒定:一个像素点随着时 ...

  6. lca(最近公共祖先(在线)) 倍增法详解

    转自大佬博客 : https://blog.csdn.net/lw277232240/article/details/72870644 描述:倍增法用于很多算法当中,通过字面意思来理解 LCA是啥呢 ...

  7. 后缀数组 DC3构造法 —— 详解

    学习了后缀数组,顺便把DC3算法也看了一下,传说中可以O(n)复杂度求出文本串的height,先比较一下倍增算法和DC3算法好辣. DC3 倍增法 时间复杂度 O(n)(但是常数很大)   O(nlo ...

  8. IPv4地址表示法详解

    在TCP/IP协议中,IP地址是一个最基本的概念,本文就来参考<计算机网络>谢希仁 这本书,总结一下IPv4地址表示法的发展阶段,做个读书笔记. IP地址的编址方法共经过了三个历史阶段: ...

  9. nginx的location、rewrite玩法详解

      1. location正则写法 一个示例: 1234567891011121314151617181920212223242526272829303132333435363738394041424 ...

随机推荐

  1. JSON.stringify初识

    1.JSON.stringify()简介: JSON.stringify()这个函数是用来序列化对象的,即是把对象类型转换成json类型. 它有三个参数,即JSON.stringify(value [ ...

  2. mongoDB 读书笔记(初级命令)

    关于mongoDB的相关知识,读书笔记,便于自己查阅用,不定期更新(纯手打)        <mongoDB权威指南> 一.创建更新和删除 1.创建 //批量插入一个集合可以节省时间,只用 ...

  3. 如何在intellj Idea中给新建的项目添加jar包?

    1. 假如我加入joda.jar 2. 找到发布的你想要的jar包,下载! 3. 解压刚下载的jar包,复制 4. 在intellj idea中新建一个java项目,然后创建一个专门用于放jar的li ...

  4. Review of Segmentation for Medical image analysis

    成像方法:X射线,CT,MRI,SPECT,PET等 分割的定义: Image segmentation is a procedure for extracting the region of int ...

  5. 发送xml或json格式的数据给服务器

    后台通过context.Request.InputStream来接收 #region 发送消息 + void SendMessage() /// <summary> /// 发送消息 // ...

  6. Java控制语句——while语句

    while循环 在循环刚开始时,会计算一次“布尔表达式”的值,若条件为真,执行循环体,而对于后来每一次额外的循环,都会在开始前重新计算一次. 注意:语句中应有使循环趋向于结束的语句,否则会出现无限循环 ...

  7. [Machine Learning]k-NN

    k-NN最近邻算法 基本思想: 对未知样本X,从训练样本集中获取与其最相近的k个样本,利用这k个样本的类别预测未知样本X的类别. 如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大 ...

  8. BeanUtils设置字段值失败问题

    package org.apache.commons.beanutils; import static org.junit.Assert.*; import java.beans.BeanInfo; ...

  9. 查看oracle 数据库的DBID

    (1)使用rman查看$ rman target /Recovery Manager: Release 11.2.0.4.0 - Production on Fri Jun 12 03:20:19 2 ...

  10. jquery对strutrs2 <s:radio>标签的设置和取值

    今天郁闷了1小时. 需求是这样的: <s:radio  list="#{0:'男',1:'女'}" value="member.sex" id=" ...