RAII是resource acquisition is initialization的缩写,意为“资源获取即初始化”。它是C++之父Bjarne Stroustrup提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在RAII的指导下,C++把底层的资源管理问题提升到了对象生命周期管理的更高层次。
说起来,RAII的含义倒也不算复杂。用白话说就是:在类的构造函数中分配资源,在析构函数中释放资源。这样,当一个对象创建的时候,构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用资源,资源的使用是安全可靠的。
C++ RAII体现出了简洁、安全、实时的特点:
1.概念简洁性:让资源(包括内存和非内存资源)和对象的生命周期绑定,资源类的设计者只需用在类定义内部处理资源问题,提高了程序的可维护性
2.类型安全性:通过资源代理对象包装资源(指针变量),并利用运算符重载提供指针运算方便使用,但对外暴露类型安全的接口
3.异常安全性:栈语义保证对象析构函数的调用,提高了程序的健壮性
4.释放实时性:和GC相比,RAII达到了和手动释放资源一样的实时性,因此可以承担底层开发的重任
也许你还在惊讶RAII如此简单的时候,关于RAII的主要内容已经介绍完了。简单不意味着简陋,在我看来RAII虽然不像GC一样,是一套具体的机制,但它蕴含的对象与资源关系的哲学深度的理解却使得我对Bjarne Stroustrup肃然起敬!
最后,不得不提醒RAII的理念固然简单,不过在具体实现的时候仍有需要小心的地方。比如对于STL的auto_ptr,可以视为资源的代理对象,auto_ptr对象间的赋值是一个需要特别注意的地方。简单说来资源代理对象间赋值的语义不满足“赋值相等”,其语义是资源管理权的转移。
什么是“赋值相等”呢?比如:
int a; int b = 10; a = b; //这句话执行后 a == b 但对于资源代理对象,这是不满足的,比如:
auto_ptr<int> a(null); auto_ptr<int> b(new int(123)); a = b; //这句话执行后a != b,赋值的语义是b把资源的管理权交给了a
auto_ptr是这样一种指针:它是“它所指向的对象”的拥有者。这种拥有具有唯一性,即一个对象只能有一个拥有者,严禁一物二主。当auto_ptr指针被摧毁时,它所指向的对象也将被隐式销毁,即使程序中有异常发生,auto_ptr所指向的对象也将被销毁。
关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr<int> p(new int(42)); //OK
std::auto_ptr<int> p = new int(42); //ERROR
这是因为auto_ptr 的构造函数被定义为了explicit
5、不要把auto_ptr放入容器
举个常见的例子:
- 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类
一般情况下,RAII临时对象不允许复制和赋值,当然更不允许在heap上创建,所以先写下一个RAII的base类,使子类私有继承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_;
- };
将Handle类做成模板类,这样就可以将class类型放入其中。另外, ResourceHandle可以根据不同资源类型的释放形式来定义不同的析构函数。
由于不能使用该类的指针,所以使用虚函数是没有意义的。
注:自己写的RAII类并没有经过大量的实践,可能存在问题,请三思而慎用。这里只是记录下自己的实现想法。