RAII(资源获取即初始化)

RAII(Resource Acquisition Is Initialization)

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

RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。

  • 资源的概念
    在计算机系统中,资源是数量有限且对系统正常运转具有一定作用的元素。比如内存,文件句柄,互斥锁(mutex locks) 等等,它们都属于系统资源。由于资源的数量不是无限的,有的资源甚至在整个系统中仅有一份,因此我们在使用资源时必须严格遵循的步骤是:
    1:获取资源
    2:使用资源
    3:释放资源

主要作用:

在不失代码简洁性的同时,可以很好地保证代码的异常安全性。

//下面的C++实例说明了如何用RAII访问文件和互斥量:

#include <string>
#include <mutex>
#include <iostream>
#include <fstream>
#include <stdexcept>
 
void write_to_file(const std::string & message)
{
    // 创建关于文件的互斥锁
    static std::mutex mutex;
 
    // 在访问文件前进行加锁
    std::lock_guard<std::mutex> lock(mutex);
 
    // 尝试打开文件
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");
 
    // 输出文件内容
    file << message << std::endl;
 
    // 当离开作用域时,文件句柄会被首先析构 (不管是否抛出了异常)
    // 互斥锁也会被析构 (同样地,不管是否抛出了异常)
}

C++保证了所有栈对象在生命周期结束时会被销毁(即调用析构函数),所以该代码是异常安全的。无论在write_to_file函数正常返回时,还是在途中抛出异常时,都会引发write_to_file函数的堆栈回退,而此时会自动调用lock和file对象的析构函数。

当一个函数需要通过多个局部变量来管理资源时,RAII就显得非常好用。因为只有被构造成功(构造函数没有抛出异常)的对象才会在返回时调用析构函数,同时析构函数的调用顺序恰好是它们构造顺序的反序,这样既可以保证多个资源(对象)的正确释放,又能满足多个资源之间的依赖关系。

由于RAII可以极大地简化资源管理,并有效地保证程序的正确和代码的简洁,所以通常会强烈建议在C++中使用它。

典型用法

1.文件句柄管理

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

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

FileHandle类的构造函数调用fopen()获取资源;FileHandle类的析构函数调用fclose()释放资源。请注意,考虑到FileHandle对象代表一种资源,它并不具有拷贝语义,因此我们将拷贝构造函数和赋值运算符声明为私有成员。如果利用FileHandle类的局部对象表示文件句柄资源,那么前面的UseFile函数便可简化为:

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

2.管理多个资源的复杂对象

例如,Widget类的构造函数要获取两个资源:文件myFile和互斥锁myLock。每个资源的获取都有可能失败并且抛出异常。
为了正常使用Widget对象,这里我们必须维护一个不变式(invariant):当调用构造函数时,要么两个资源全都获得,对象创建成功;要么两个资源都没得到,对象创建失败。获取了文件而没有得到互斥锁的情况永远不能出现,也就是说,不允许建立Widget对象的半成品。如果将RAII惯用法应用于成员对象,那么我们就可以实现这个不变式:

class Widget{
public:
	Widget(const char* myFile,const char* myLock):file_(myFile),lock_(mylock){ }
	//...
private:
	FileHandle file_;
	LockHandle lock_;
};

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

3.lock_guard

RAII在C++中的应用非常广泛,如C++标准库中的lock_guard便是用RAII方式来控制互斥量:

template <class Mutex> class lock_guard {
private:
    Mutex& mutex_;

public:
    lock_guard(Mutex& mutex) : mutex_(mutex) { mutex_.lock(); }
    ~lock_guard() { mutex_.unlock(); }

    lock_guard(lock_guard const&) = delete;
    lock_guard& operator=(lock_guard const&) = delete;
};

程序员可以非常方便地使用lock_guard,而不用担心异常安全问题

extern void unsafe_code();  // 可能抛出异常

using std::mutex;
using std::lock_guard;

mutex g_mutex;

void access_critical_section()
{
    lock_guard<mutex> lock(g_mutex);
    unsafe_code();
}

总结

  • RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放
  • 拥有对象就等于拥有资源,对象存在则资源必定存在。由此可见,RAII惯用法是进行资源管理的有力武器。
  • C++程序员依靠RAII写出的代码不仅简洁优雅,而且做到了异常安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值