C/C++学习笔记 资源获取是初始化 (RAII) 理解
一、RAII概述
也许RAII 的一个更好的名称是“范围绑定资源”,因为您将资源的生命周期与局部变量的生命周期联系起来,并且局部变量的生命周期在它超出范围时结束。
现代 C++ 通过在堆栈上声明对象来尽可能避免使用堆内存。当资源对于堆栈来说太大时,它应该由一个对象拥有。当对象被初始化时,它会获取它拥有的资源。然后该对象负责在其析构函数中释放资源。拥有对象本身在堆栈上声明。对象拥有资源的原则也称为“资源获取即初始化”或RAII。
当拥有资源的堆栈对象超出范围时,会自动调用其析构函数。这样,C++ 中的垃圾回收与对象的生命周期密切相关,并且是确定性的。资源总是在程序中的已知点释放,您可以控制该点。只有像 C++ 中那样的确定性析构函数才能平等地处理内存和非内存资源。
RAII 保证资源可用于任何可能访问对象的函数(资源可用性是类不变量,消除了多余的运行时测试)。它还保证所有资源在其控制对象的生命周期结束时以获取相反的顺序释放。同样,如果资源获取失败(构造函数异常退出),则每个完全构造的成员和基础子对象获取的所有资源都以初始化的相反顺序释放。这利用了核心语言特性(对象生存期、范围退出、初始化顺序和堆栈展开)) 以消除资源泄漏并保证异常安全。该技术的另一个名称是范围绑定资源管理(SBRM),在基本用例之后,RAII 对象的生命周期由于范围退出而结束。
RAII可以总结如下:
将每个资源封装到一个类中,其中构造函数获取资源并建立所有类不变量,如果无法完成则抛出异常,析构函数释放资源并且从不抛出异常;
始终通过 RAII 类的实例使用资源,本身具有自动存储期限或临时生命周期,或具有受自动或临时对象的生命周期限制的生命周期。
RAII 类包含三个部分:
资源在析构函数中被放弃(例如关闭文件)
类的实例是堆栈分配的
在构造函数中获取资源(例如打开文件)。这部分是可选的。
二、示例1:确保实例分配在堆栈上
class OpenFile {
public:
OpenFile(const char* filename){
//失败则抛出异常
_file.open(filename);
}
~OpenFile(){
_file.close();
}
std::string readLine() {
return _file.readLine();
}
private:
File _file;
};
OpenFile f("boo.txt");
//异常安全,并且没有必要进行关闭
loadFromFile(f);
确保实例分配在堆栈上而不是堆上
std::string firstLineOf(const char* filename){
OpenFile f("boo.txt"); //stack allocated
return f.readLine();
//文件在这里关闭。 `f` 超出范围并运行析构函数。
}
std::string firstLineOf(const char* filename){
OpenFile* f = new OpenFile("boo.txt"); //heap allocated
return f->readLine();
//析构函数永远不会运行,因为 `f` 永远不会被删除
}
三、示例2:坏的示例和好的示例
std::mutex m;
//这是一个坏的示例
void bad()
{
m.lock(); // acquire the mutex
f(); // if f() throws an exception, the mutex is never released
if(!everything_ok()) return; // early return, the mutex is never released
m.unlock(); // if bad() reaches this statement, the mutex is released
}
//这是一个好的示例
void good()
{
std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
f(); // if f() throws an exception, the mutex is released
if(!everything_ok()) return; // early return, the mutex is released
} // if good() returns normally, the mutex is released
四、示例3:使用智能指针的示例
1、简单对象
下面的例子展示了一个简单的对象w。它在函数范围的堆栈上声明,并在函数块的末尾被销毁。该对象w不拥有任何资源(例如堆分配的内存)。它唯一的成员g本身在堆栈上声明,并且与 . 一起超出范围w。widget析构函数中不需要特殊代码。
class widget {
private:
gadget g; // 生命周期自动绑定到封闭对象
public:
void draw();
};
void functionUsingWidget () {
widget w; // 生命周期自动绑定到封闭范围构造 w,包括 w.g 成员
// ...
w.draw();
// ...
} // w 和 w.g 自动异常安全的自动销毁和释放,就像“finally { w.dispose(); w.g.dispose(); }”
2、手动释放资源
在以下示例中,w拥有内存资源,因此必须在其析构函数中包含代码才能释放内存。
class widget
{
private:
int* data;
public:
widget(const int size) { data = new int[size]; } // acquire
~widget() { delete[] data; } // release
void do_something() {}
};
void functionUsingWidget() {
widget w(1000000); // 生命周期自动绑定到封闭范围构造 w,包括 w.data 成员
w.do_something();
} //w 和 w.data 的自动销毁和释放
3、使用智能指针
从 C++11 开始,有一种更好的方法来编写前面的示例:使用标准库中的智能指针。智能指针处理它所拥有的内存的分配和删除。使用智能指针消除了在widget类中显式析构函数的需要。
#include <memory>
class widget
{
private:
std::unique_ptr<int[]> data;
public:
widget(const int size) { data = std::make_unique<int[]>(size); }
void do_something() {}
};
void functionUsingWidget() {
widget w(1000000); // 生命周期自动绑定到封闭范围构造 w,包括 w.data 小工具成员
// ...
w.do_something();
// ...
} // w 和 w.data 的自动销毁和释放
通过使用智能指针进行内存分配,您可以消除内存泄漏的可能性。此模型适用于其他资源,例如文件句柄或套接字。您可以在类中以类似的方式管理自己的资源。
C++ 的设计确保对象在超出范围时被销毁。也就是说,它们会随着块的退出而被破坏,与构建的顺序相反。当一个对象被销毁时,它的基础和成员会以特定的顺序被销毁。在任何块之外、在全局范围内声明的对象可能会导致问题。如果全局对象的构造函数抛出异常,可能很难调试。