RAII(资源获取即初始化)详解


概念

使用局部对象管理资源的技术通常称为“资源获取就是初始化”

Resource Acquisition Is Initialization 机制是Bjarne Stroustrup首先提出的。要解决的是这样一个问题:

C++中,如果在这个程序段结束时需要完成一些资源释放工作,那么正常情况下自然是没有什么问题,但是当一个异常抛出时,释放资源的语句就不会被执行。于是Bjarne Stroustrup就想到确保能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。将初始化和资源释放都移动到一个包装类中的好处:

  • 保证了资源的正常释放
  • 省去了在异常处理中冗长而重复甚至有些还不一定执行到的清理逻辑,进而确保了代码的异常安全。
  • 简化代码体积。

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

管理局部对象的任务非常简单,因为它们的创建和销毁工作是由系统自动完成的。我们只需在某个作用域(scope)中定义局部对象(这时系统自动调用构造函数以创建对象),然后就可以放心大胆地使用之,而不必担心有关善后工作;当控制流程超出这个作用域的范围时,系统会自动调用析构函数,从而销毁该对象。

将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务。这就是RAII惯用法的真谛!

应用场景

  1. 文件操作

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

  fclose(fp);
}  

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

此时,就可以让RAII惯用法大显身手了。

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

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

比如文件操作的例子,我们的RAII临时对象类就可以写成:

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

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

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

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_;
};

FileHandleLockHandle类的对象作为Widget类的数据成员,分别表示需要获取的文件和互斥锁。资源的获取过程就是两个成员对象的初始化过程。在此系统会自动地为我们进行资源管理,程序员不必显式地添加任何异常处理代码。例如,当已经创建完file_,但尚未创建完lock_时,有一个异常被抛出,则系统会调用file_的析构函数,而不会调用lock_的析构函数。Bjarne所谓构造函数和析构函数“与异常处理的交互作用”,说的就是这种情形。

RAII的本质内容是用对象代表资源把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。由此可见,RAII惯用法是进行资源管理的有力武器。C++程序员依靠RAII写出的代码不仅简洁优雅,而且做到了异常安全。难怪微软的MSDN杂志在最近的一篇文章中承认:“若论资源管理,谁也比不过标准C++”。

创建自己的RAII类

一般情况下,RAII临时对象不允许复制和赋值,当然更不允许在heap上创建,所以先写下一个RAIIbase类,使子类私有继承Base类来禁用这些操作:

class RAIIBase
{
public:
    RAIIBase(){}
    ~RAIIBase(){}//由于不能使用该类的指针,定义虚函数是完全没有必要的  

    RAIIBase (const RAIIBase &);
    RAIIBase & operator = (const RAIIBase &);
    void * operator new(size_t size);
    // 不定义任何成员
};  

当我们要写自己的RAII类时就可以直接继承该类的实现:

template<typename T>
class ResourceHandle: private RAIIBase //私有继承 禁用Base的所有继承操作
{
public:
    explicit ResourceHandle(T * aResource):r_(aResource){}//获取资源
    ~ResourceHandle() {delete r_;} //释放资源
    T *get()    {return r_ ;} //访问资源
private:
    T * r_;
};


我们可以是用这个机制将文件操作包装起来完成一个异常安全的文件类。实现上,注意将复制构造函数和赋值符私有化,这个是通过一个私有继承类完成的,因为这两个操作在此并没有意义,当然这并不是RAII所要求的。

#include <IOSTREAM>
#include <STDEXCEPT>
#include <CSTDIO>

using namespace std;
class NonCopyable
{
public:
NonCopyable(){};
private:
    NonCopyable (NonCopyable const &); // private copy constructor
    NonCopyable & operator = (NonCopyable const &); // private assignment operator
};

class SafeFile:NonCopyable{
public:
    SafeFile(const char* filename):fileHandler(fopen(filename,"w+"))
    {
        if( fileHandler == NULL )
        {
            throw runtime_error("Open Error!");
        }
    }
    ~SafeFile()
    {
        fclose(fileHandler);
    }

    void write(const char* str)
    {
        if( fputs(str,fileHandler)==EOF )
        {
            throw runtime_error("Write Error!");
        }
    }

    void write(const char* buffer, size_t num)
    {
        if( num!=0 && fwrite(buffer,num,1,fileHandler)==0 )
        {
            throw runtime_error("Write Error!");
        }
    }
private:
    FILE *fileHandler;
    SafeFile(const SafeFile&);
    SafeFile &operator =(const SafeFile&);
};

int main(int argc, char *argv[])
{
    SafeFile testVar("foo.test");
    testVar.write("Hello RAII");
}

C++的结构决定了其原生支持RAII,而在Java 中,对象何时销毁是未知的,所以在Java 中可以使用try-finally做相关处理。

  1. 智能指针模拟

一个更复杂一点的例子是模拟智能指针,抽象出来的RAII类中实现了一个操作符*,直接返回存入的指针:

现在我们有一个类:

class Example {
  SomeResource* p_;
  SomeResource* p2_;
public:
  Example() :
    p_(new SomeResource()),
    p2_(new SomeResource()) {
    std::cout << "Creating Example, allocating SomeResource!/n";
  }

  Example(const Example& other) :
    p_(new SomeResource(*other.p_)),
    p2_(new SomeResource(*other.p2_)) {}

  Example& operator=(const Example& other) {
    // Self assignment?
    if (this==&other)
      return *this;

    *p_=*other.p_;
    *p2_=*other.p2_;
    return *this;
  }

  ~Example() {
     std::cout << "Deleting Example, freeing SomeResource!/n";
     delete p_;
     delete p2_;
  }
};

假设在创建SomeResource的时候可能会有异常,那么当p_指向的资源被创建但p2_指向的资源创建失败时,Example的实例就整个创建失败,那么p_指向的资源就存在内存泄露问题。

用下边的这个方法可以为权宜之计:

Example() : p_(0),p2_(0)
{
  try {
    p_=new SomeResource();
    p2_=new SomeResource("H",true);
    std::cout << "Creating Example, allocating SomeResource!/n";
  }
  catch(...) {
    delete p2_;
    delete p_;
    throw;
  }
}

但是我们可以利用一个对象在离开一个域中会调用析构函数的特性,在构造函数中完成初始化,在析构函数中完成清理工作,将需要操作和保护的指针作为成员变量放入RAII中。

template <TYPENAME T>
class RAII {
  T* p_;
public:
  explicit RAII(T* p) : p_(p) {}

  ~RAII() {
    delete p_;
  }

  void reset(T* p) {
    delete p_;
    p_=p;
  }

  T* get() const {
     return p_;
  }

  T& operator*() const {
     return *p_;
  }

  void swap(RAII& other) {
    std::swap(p_,other.p_);
  }

private:
  RAII(const RAII& other);
  RAII& operator=(const RAII& other);
};

我们在具体使用把保护的指针Someresource放在RAII中:

 class Example {
  RAII<SOMERESOURCE> p_;
  RAII<SOMERESOURCE> p2_;
public:
  Example() :
    p_(new SomeResource()),
    p2_(new SomeResource()) {}

  Example(const Example& other)
    : p_(new SomeResource(*other.p_)),
      p2_(new SomeResource(*other.p2_)) {}

  Example& operator=(const Example& other) {
    // Self assignment?
    if (this==&other)
      return *this;

    *p_=*other.p_;
    *p2_=*other.p2_;
    return *this;
  }

  ~Example() {
    std::cout << "Deleting Example, freeing SomeResource!/n";
  }
};

现在即使p_成功而p2_失败,那么在Stack winding时也会调用RAII的析构函数保证了p_指向的Someresource被析构。这种方法较之例1中需要实现被组合的指针类型相应的接口不同,这里不需要对接口进行封装。当然,在例1中,你也可以提供一个getPointer的函数直接将句柄提供出来。

其实在Example中,已经不需要析构函数了,因为RAII类会帮它照顾好这一切的。这有点像auto_ptr,本文并不打算深入讨论智能指针这个话题。

锁操作

#include <CSTDIO>
#include <STDLIB.H>
#include <PTHREAD.H>

int counter = 0;
void* routine(void *ptr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

class NonCopyable
{
public:
    NonCopyable(){};
private:
    NonCopyable (NonCopyable const &); // private copy constructor
    NonCopyable & operator = (NonCopyable const &); // private assignment operator
};

class ScopeMutex:NonCopyable
{
public:
    ScopeMutex(pthread_mutex_t* mutex):mutex_(mutex){
        pthread_mutex_lock( mutex_ );
    }

    ~ScopeMutex(){
        pthread_mutex_unlock( mutex_ );
    }
private:
    pthread_mutex_t *mutex_;
};

int main(int argc, char *argv[])
{
    int rc1, rc2;
    pthread_t thread1, thread2;
    if( (rc1=pthread_create( &thread1, NULL, routine, NULL)) )
    {
        printf("Thread creation failed: %d/n", rc1);
    }

    if( (rc2=pthread_create( &thread2, NULL, routine, NULL)) )
    {
        printf("Thread creation failed: %d/n", rc1);
    }
    pthread_join( thread1, NULL);
    pthread_join( thread2, NULL);
}

void* routine(void *ptr)
{
    ScopeMutex scopeMutex(&mutex);
    counter++;
    printf("%d/n",counter);
}

总结

RAII机制保证了异常安全,并且也为程序员在编写动态分配内存的程序时提供了安全保证。缺点是有些操作可能会抛出异常,如果放在析构函数中进行则不能将错误传递出去,那么此时析构函数就必须自己处理异常。这在某些时候是很繁琐的。

时间: 2024-10-02 20:09:04

RAII(资源获取即初始化)详解的相关文章

qt的资源替换搜索QDir详解

QDir对跨平台的目录操作提供了很多的便利,为了更加方便的提供全局资源的查找,QDir提供了搜索路径替换功能,解决了资源搜索不便的问题,也能提高文件查找的效率. QDir通过已知的路径前缀去搜索并定位文件,搜索路径增加是有序的.从第一个设置的搜索路径开始,是不是觉得和cocos2d的路径搜索非常相似呢. 见如下QT的原版例子 QDir::setSearchPaths("icons", QStringList(QDir::homePath() + "/images")

servlet资源路径加载详解(3)

1.对于servlet的读取资源文件,要考虑工程文件路径和tomcat服务器的部署后的文件,所以一定要弄清楚加载文件路径位置,同时要考虑采用哪种方式: 一般有三种方式: (1).采用传统方式即jdk中提供的InputStream is = new FileInputStream("config.properties"); (2).采用ServletContext对象加载 (3).采用类加载器的方式 2.用代码检验 package cn.wwh.www.web.servlet; impo

springmvc RequestMappingHandlerAdapter初始化详解

我们来看一下RequestMappingHandlerAdapter初始化时做了什么? initControllerAdviceCache()方法是处理注解@ControllerAdvice的,此时我们暂且不关注 这个就比较关键了,注解了很多参数解析器,后文详解#1 返回用于@initbinder方法的参数解析器列表,包括内置的和自定义的解析器.注册步骤和上一步如出一辙不再详述 注册了很多返回值处理器,注册步骤和上一步如出一辙不再详述 书接前文#1 注册了基于注解的参数解析器包括注解@PathV

unity3d 获取游戏对象详解

原文地址:http://www.xuanyusong.com/archives/2768 我觉得Unity里面的Transform 和 GameObject就像两个双胞胎兄弟一样,这俩哥们很要好,我能直接找到你,你也能直接找到我.我看很多人喜欢在类里面去保存GameObject对象.解决GameObject.Find()无法获取天生activie = false的问题.     private GameObject root ; 我觉得你最好不要保存GameObject ,而是去保存Transf

springmvc RequestMappingHandlerMapping初始化详解

springmvc中配置这个标签默认注册三个bean:RequestMappingHandlerMapping,RequestMappingHandlerAdapter,DefaultHandlerExceptionResolver RequestMappingHandlerMapping 我们看它实现了InitializingBean 接口,所以在getBean()实例化它时会执行afterPropertiesSet()方法,来看该方法干了什么? 方法中实例化了一个BuilderConfigu

资源获取即初始化RAII

//class Resource { //public: // Resource(parms p): r(allocate(p)) { } // ~Resource() { release(r); } // // also need to define copy and assignment //private: // resource_type *r; // resource managed by this type // resource_type *allocate(parms p); /

插件化框架解读之Android 资源加载机制详解(二)

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680Android提供了一种非常灵活的资源系统,可以根据不同的条件提供可替代资源.因此,系统基于很少的改造就能支持新特性,比如Android N中的分屏模式.这也是Android强大部分之一.本文主要讲述Android资源系统的实现原理,以及在应用开发中需要注意的事项. 一.定义资源 Android使用XML文件描述各种资源,包括字符串.颜色.尺寸.主题.布局.甚至是

Php学习之资源类型的使用详解

本文和大家分享的主要是php中资源类型的使用相关内容,一起来看看吧,希望对大家 学习php 有所帮助.  资源类型是一种特殊类型,它实际上可以保存任意的C指针,对PHP表现出一个资源对象的模样,例如:PHP里fopen的返回值就是一个resource.  我们可以利用资源类型,保存类型对象的指针,比如:一个FILE*文件描述符,或者仅仅是一个简单的char *字符串,其意义是可以将我们希望传递的C语言内存对象通过zval的形式包装起来,以便C和PHP跨语言传递.  资源类型是一个zval的底层数

C语言数组空间的初始化详解

数组空间的初始化就是为每一个标签地址赋值.按照标签逐一处理.如果我们需要为每一个内存赋值,假如有一个int a[100];我们就需要用下标为100个int类型的空间赋值.这样的工作量是非常大的,我们就想到了让编译器做一些初始化操作,初始化操作是第一次赋值,第二次赋值就不能再这样赋值了. int a[10]=空间: 我需要给它一个空间,让它对这里面的值进行批量处理:比如int a[10]={10,20,30}; //a[1]=10,a[2]=20,a[3]=30,a[4]=-=a[9]=0所以实际