C++中RAII机制的介绍与简单实例

今天看陈硕的多线程书上提到了C++中RAII技术的使用,通过用C11里面自带的智能指针来完成对资源的控制,但是一直不太清楚具体RAII是怎么样的,带着这样的疑问,特地去看了几篇博客,找了一个简单的文件句柄打开关闭RAII管理的实例,瞬间就明白了,这里分享出来。主要从两个部分,首先是RAII技术的介绍,然后是RAII技术的简单实例。

RAII技术的介绍

1、RAII定义

RAII,它是“Resource Acquisition Is Initialization”的首字母缩写。也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。

RAII的好处在于它提供了一种资源自动管理的方式,当产生异常、回滚等现象时,RAII可以正确地释放掉资源。

当讲述C++资源管理时,Bjarne这样写道:

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

2、RAII的理解

看到上面RAII的介绍,发现这不就是栈的资源回收过程中吗??

没错,思想的确是一样的。

我们知道在函数内部的一些成员是放置在栈空间上的,当函数返回时,这些栈上的局部变量就会立即释放空间,于是Bjarne Stroustrup就想到确保能运行资源释放代码的地方就是在这个程序段(栈)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。RAII就利用了栈里面的变量的这一特点。

操作系统中栈的操作过程
Stack Winding & Unwinding
当程序运行时,每一个函数(包括数据、寄存器、程序计数器,等等)在调用时,都被映射到栈上。这就是 stack winding。
Unwinding 是以相反顺序把函数从栈上移除的过程。
正常的 stack unwinding 发生在函数返回时;不正常的情况,比如引发异常,调用setjmp和longjmp,也会导致 stack unwinding。
可见 stack unwinding 的过程中,局部对象的析构函数将逐一被调用。这也就是 RAII 工作的原理,它是由语言和编译器来保证的。

3、RAII做法

RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个存放在栈空间上的局部对象。
这种做法有两大好处:
(1)不需要显式地释放资源。
(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。

那么给出了RAII的做法,问题就是什么是资源,我们来明确资源的概念,在计算机系统中,资源是数量有限且对系统正常运转具有一定作用的元素。

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

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

获取资源
使用资源
释放资源

总结
使用RAII,需要自己定义资源类,将自己业务的操作资源封装起来,然后通过这个资源类来完成资源的构造、使用、释放。这样让我们可以放心的去编写逻辑功能的代码,而不用去关心会不会造成内存泄漏这样的问题。

RAII技术的简单实例

正如上面介绍的RAII技术,这里我们给出一个简单的文件句柄打开关闭RAII管理的实例。
一个简单的句柄文件打开关闭的程序

void Func() 
{ 
  FILE *fp; 
  char* filename = "test.txt"; 
  if((fp=fopen(filename,"r"))==NULL) 
  { 
      printf("not open"); 
      exit(0); 
  } 
  ... // 如果 在使用fp指针时产生异常 并退出 
       // 那么 fp文件就没有正常关闭 
       
  fclose(fp); 
}

在资源的获取到释放之间,我们往往需要使用资源,但常常一些不可预计的异常是在使用过程中产生,就会使资源的释放环节没有得到执行。

可能我们会在每个分支上进行关闭操作,来保证资源的正常释放。

  FILE *fp; 
  char* filename = "test.txt"; 
  if((fp=fopen(filename,"r"))==NULL) 
  { 
      printf("not open"); 
      exit(0); 
  } 
  if (.....)
    {
        fclose(fp);			//在每个分支上都关闭fp
        return;
    }
    else if(.....)
    {
        fclose(fp);			//在每个分支上都关闭fp
        return;
    }
...
  fclose(fp); 

使用这种方法,每个分支你都要去释放资源,不仅代码会很冗余,同时可能在分支较多的情况下,你会忘记释放,这是十分常见的现象。
此时,就可以让RAII惯用法大显身手了。

使用RAII去管理资源的创建释放
RAII的实现原理很简单,利用stack上的临时对象生命期是程序自动管理的这一特点,将我们的资源释放操作封装在一个临时对象中。
例如:

class Resource{}; 
class RAII{ 
public: 
    RAII(Resource* aResource):r_(aResource){} //获取资源 
    ~RAII() {delete r_;} //释放资源 
    Resource* get()    {return r_ ;} //访问资源 
private: 
    Resource* r_; 
}; 

那么对于上述的文件句柄打开关闭的例子,我们可以写成如下的资源类:

class FileRAII{  
public:  
    FileRAII(FILE* aFile):file_(aFile){}  
    ~FileRAII() { fclose(file_); }//在析构函数中进行文件关闭  
    FILE* get() {return file_;}  
private:  
    FILE* file_;  
}; 

则上面这个打开文件的例子就可以用RAII改写为:

class FileRAII{  
public:  
    FileRAII(FILE* aFile):file_(aFile){}  
    ~FileRAII() { fclose(file_); }//在析构函数中进行文件关闭  
    FILE* get() {return file_;}  
private:  
    FILE* file_;  
}; 

void Func()  
{  
  FILE *fp;  
  char* filename = "test.txt";  
  if((fp=fopen(filename,"r"))==NULL)  
  {  
      printf("not open");  
      exit(0);  
  }  
  FileRAII fileRAII(fp);  
  ... // 如果 在使用fp指针时产生异常 并退出  
       // 那么 fileRAII在栈展开过程中会被自动释放,析构函数也就会自动地将fp关闭  
 
  // 即使所有代码是都正确执行了,也无需手动释放fp,fileRAII它的生命期在此结束时,它的析构函数会自动执行!      
 } 

这就是RAII的魅力,它免除了对需要谨慎使用资源时而产生的大量维护代码。在保证资源正确处理的情况下,还使得代码的可读性也提高了不少。

参考博客:
https://www.cnblogs.com/jiangbin/p/6986511.html
https://www.cnblogs.com/Allen-rg/p/6891971.html
https://blog.csdn.net/wozhengtao/article/details/52187484
https://blog.csdn.net/gettogetto/article/details/60878776

ps:这里使用一个简单的例子说明,没有涉及文章最开始说到的C11里面的智能指针,具体怎么将智能指针加入到这个RAII机制中来,我也在学习,后续。。。。。

猜你喜欢

转载自blog.csdn.net/u012414189/article/details/84980339