“RAII资源获取就是初始化”的好处

RAII指的是“资源获取就是初始化”(Resource Allocation Is Initialization),它被视作C++中最强大的编程范式之一
简单说来,它指的是,用构造函数来获取一个对象的资源,相应的,借助析构函数来释放对象的资源

为了理解这一范式的用处,让我们考虑某个函数使用文件句柄时的情况:

void doSomethingWithAFile(const char* filename)
{
    // 首先,让我们假设一切都会顺利进行。

    FILE* fh = fopen(filename, "r"); // 以只读模式打开文件

    doSomethingWithTheFile(fh);
    doSomethingElseWithIt(fh);

    fclose(fh); // 关闭文件句柄
}

不幸的是,随着错误处理机制的引入,事情会变得复杂。
假设fopen函数有可能执行失败,而doSomethingWithTheFiledoSomethingElseWithIt会在失败时返回错误代码。
(虽然异常是C++中处理错误的推荐方式,但是某些程序员,尤其是有C语言背景的,并不认可异常捕获机制的作用)。
现在,我们必须检查每个函数调用是否成功执行,并在问题发生的时候关闭文件句柄

bool doSomethingWithAFile(const char* filename)
{
    FILE* fh = fopen(filename, "r"); // 以只读模式打开文件
    if (fh == nullptr) // 当执行失败是,返回的指针是nullptr
        return false; // 向调用者汇报错误

    // 假设每个函数会在执行失败时返回false
    if (!doSomethingWithTheFile(fh)) {
        fclose(fh); // 关闭文件句柄,避免造成内存泄漏。
        return false; // 反馈错误
    }
    if (!doSomethingElseWithIt(fh)) {
        fclose(fh); // 关闭文件句柄
        return false; // 反馈错误
    }

    fclose(fh); // 关闭文件句柄
    return true; // 指示函数已成功执行
}

C语言的程序员通常会借助goto语句简化上面的代码:

bool doSomethingWithAFile(const char* filename)
{
    FILE* fh = fopen(filename, "r");
    if (fh == nullptr)
        return false;

    if (!doSomethingWithTheFile(fh))
        goto failure;

    if (!doSomethingElseWithIt(fh))
        goto failure;

    fclose(fh); // 关闭文件
    return true; // 执行成功

failure:
    fclose(fh);
    return false; // 反馈错误
}

如果用异常捕获机制来指示错误的话,代码会变得清晰一些,但是仍然有优化的余地。

void doSomethingWithAFile(const char* filename)
{
    FILE* fh = fopen(filename, "r"); // 以只读模式打开文件
    if (fh == nullptr)
        throw std::exception("Could not open the file.");

    try {
        doSomethingWithTheFile(fh);
        doSomethingElseWithIt(fh);
    }
    catch (...) {
        fclose(fh); // 保证出错的时候文件被正确关闭
        throw; // 之后,重新抛出这个异常
    }

    fclose(fh); // 关闭文件
    // 所有工作顺利完成
}

相比之下,使用C++中的文件流类(fstream)时,fstream会利用自己的析构器来关闭文件句柄。只要离开了某一对象的定义域,它的析构函数就会被自动调用

void doSomethingWithAFile(const std::string& filename)
{
    // ifstream是输入文件流(input file stream)的简称
    std::ifstream fh(filename); // 打开一个文件

    // 对文件进行一些操作
    doSomethingWithTheFile(fh);
    doSomethingElseWithIt(fh);

} // 文件已经被析构器自动关闭

与上面几种方式相比,这种方式有着明显的优势:

  1. 无论发生了什么情况,资源(此例当中是文件句柄)都会被正确关闭。
    只要你正确使用了析构器,就不会因为忘记关闭句柄,造成资源的泄漏。
  2. 可以注意到,通过这种方式写出来的代码十分简洁。
    析构器会在后台关闭文件句柄,不再需要你来操心这些琐事。
  3. 这种方式的代码具有异常安全性。
    无论在函数中的何处拋出异常,都不会阻碍对文件资源的释放。

地道的C++代码应当把RAII的使用扩展到各种类型的资源上,包括:

  • 用unique_ptr和shared_ptr管理的内存
  • 各种数据容器,例如标准库中的链表、向量(容量自动扩展的数组)、散列表等;
    当它们脱离作用域时,析构器会自动释放其中储存的内容。
  • 用lock_guard和unique_lock实现的互斥
posted @ 2024-09-23 16:47  guanyubo  阅读(16)  评论(0编辑  收藏  举报