liangbm3's blog

Back

RAII“Resource Acquisition Is Initialization” 的缩写,中文通常翻译为 “资源获取即初始化”。这是 C++ 语言中的一种重要编程范式。

1. 核心思想#

RAII的核心思想是将资源的生命周期与对象的生命周期绑定起来。具体来说:

  1. 获取资源:在对象的构造函数中获取资源。当对象被成功创建时,资源也就被成功获取了。
  2. 使用资源:在对象的生命周期内,可以通过对象的方法来使用该资源。
  3. 释放资源:在对象的析构函数中释放资源。当对象离开其作用域(例如,函数返回、对象被 delete、栈展开等)时,其析构函数会自动被调用,从而确保资源被释放。

2. RAII的优点#

由于C/C++是需要手动管理资源的语言,保证在任何情况下都能正确释放资源是一个非常大的挑战,例如:

  • 函数提前返回:一个函数可能有多个返回路径,函数可能提前返回,导致资源没有被正确释放。
    void process_data_bad(int size) 
    {
        // 获取资源
        int* data = (int*)malloc(size * sizeof(int));
        // 检查一个条件,可能导致提前返回
        if (size <= 0) 
        {
            std::cout << "Invalid size, returning early.\n";
            // 错误:在这里返回,但没有释放内存!
            return;
        }
        // ... 使用 data ...
        std::cout << "Processing data...\n";
        //手动释放资源
        free(data);
    }
    cpp
  • 异常抛出:如果在获取资源后,在释放前发生了异常,正常的清理代码会被跳过,导致资源发生泄露。
    void process_with_exception_bad() 
    {
        // 获取资源
        char* buffer = new char[1024];
        std::cout << "Buffer allocated.\n";
        try 
        {
            // ... 使用 buffer ...
            do_some_work(true); // 模拟一个操作抛出异常
            // 异常发生后,下面的代码不会执行
            std::cout << "This line will not be printed.\n";
            delete[] buffer; // 释放代码被跳过!
        } 
        catch (const std::exception& e) 
        {
            std::cout << "Caught exception: " << e.what() << "\n";
            // 我们在这里捕获了异常,但 buffer 已经泄漏了!
        }
    }
    cpp
  • 复杂的逻辑分支:在多层的if-else循环中,要确保所有可能的代码路径上都释放资源,代码会变得非常混乱且容易出错。
    void complex_logic_bad(const char* filename, int condition) 
    {
        // 获取资源
        FILE* file = fopen(filename, "w");
        if (!file) 
        {
            return;
        }
        if (condition == 1) 
        {
            fprintf(file, "Condition 1");
            // ... 更多操作
            if (/* 某个子条件 */ true) {
                fclose(file); // 清理点 1
                return;
            }
        } 
        else if (condition == 2) 
        {
            for (int i = 0; i < 5; ++i) 
            {
                if (i == 3) 
                {
                    fprintf(file, "Breaking loop");
                    fclose(file); // 清理点 2
                    return; // 用 return 代替 break
                }
            }
        } 
        else 
        {
            fprintf(file, "Default condition");
            // 假设这里一切正常
        }
        // 最终的清理点
        fclose(file); // 清理点 3
    }
    cpp

在这些情况下,忘记或者无法调用释放资源的代码都会导致资源泄漏。而通过RAII的思想可以避免以上情况的发生,RAII主要有以下优点:

  • 自动资源管理和防止泄漏:这是 RAII最核心的优点。资源在对象销毁时自动释放,作为程序员的我们不需要手动调用释放资源的函数,如free()fclose()close_socket()pthread_mutex_unlock()等,这可以极大降低忘记释放资源导致资源泄漏的风险。
  • 异常安全:如果在资源被获取后、显示释放代码执行前发生异常,传统的try-catch-finally很容易出错或者发生遗漏。如果我们使用RAII,异常发生会导致栈展开,作用域内的析构函数会被异常调用,保证了资源能够被正确释放。
  • 代码简洁和可读性:RAII把资源管理都封装在了专门的类中,使得业务代码逻辑代码更加干净整洁。

3. RAII的经典例子#

C++标准库本身就是RAII思想的最佳实践者,下面是一些经典示例:

  • 智能指针std::unique_ptrstd::shared_ptrstd::weak_ptr是RAII的完美体现。它们在构造时获取动态分配的内存(或接管已分配的内存),并在析构时自动释放这块内存。
    #include <memory>
    #include <stdexcept>
    
    void good_function() 
    {
        // 资源获取(内存分配)即初始化
        std::unique_ptr<int> ptr(new int(42));
    
        // ... 其它操作
        if (/* 某个条件失败 */) 
        {
            // ptr 离开作用域,其析构函数被调用,自动 delete 内存
            return;
        }
        if (/* 某个操作抛出异常 */) 
        {
            // 发生异常,栈展开,ptr 的析构函数被调用,自动 delete 内存
            throw std::runtime_error("Something went wrong!");
        }
        // ... 其它操作
    
    } // 函数正常结束,ptr 离开作用域,析构函数被调用,自动 delete 内存
    cpp
  • 文件处理std::fstreamstd::ifstreamstd::ofstream类就是RAII的例子。它们在构造时打开文件,在析构时自动关闭文件。
    #include <fstream>
    #include <string>
    #include <iostream>
    
    void write_to_file(const std::string& filename) 
    {
        // 构造 ofstream 对象时,文件被打开(获取资源)
        std::ofstream file(filename);
        if (!file.is_open()) 
        {
            std::cerr << "Failed to open file.\n";
            return;
        }
    
        file << "Hello, RAII!\n";
        // ... 可能发生异常
    
    } // 函数结束,file 对象被销毁,其析构函数自动调用 file.close()
    cpp
  • 互斥锁std::lock_guardstd::unique_lock是用于管理互斥锁的RAII包装器。它们在构造时锁定互斥锁,在析构时自动解锁互斥锁,确保即使在临界区发生异常,锁也会被释放,避免死锁。
    #include <mutex>
    
    std::mutex mtx;
    
    void safe_critical_section() 
    {
        // 构造 lock_guard 时,自动调用 mtx.lock()
        std::lock_guard<std::mutex> guard(mtx);
    
        // ... 执行临界区代码
        if (/* 某个条件 */) 
        {
            // guard 离开作用域,其析构函数自动调用 mtx.unlock()
            return;
        }
        // ... 即使这里抛出异常,析构函数也会被调用
    
    } // 函数结束,guard 被销毁,析构函数自动调用 mtx.unlock()
    cpp

4. RAII类的实现#

如果我们想实现一个RAII类,一般的步骤如下:

  1. 确定资源:明确要使用这个类管理什么资源,这个资源必须有清晰的获取和释放操作,例如:fopen/fclosenew/deletelock/unlock
  2. 定义类骨架:定义一个类,并将代表资源的句柄作为其私有成员变量。
  3. 实现构造函数:在构造函数中执行获取资源的操作,构造函数应该接收所有获取资源所必须的参数,获取资源后将句柄存放在成员变量中。同时要处理资源获取失败的情况。
  4. 实现析构函数:在析构函数中执行释放资源的操作,析构函数应该检查句柄是否有效。
  5. 管理所有权:通常来说,应该RAII对象应该独占它所管理的资源,所以我们应该禁止拷贝构造函数和拷贝赋值运算符,防止两个对象管理同一个资源,避免一个资源被释放两次。通常我们还会实现移动构造函数和移动赋值运算符,允许资源所有权从一个对象转移到另一个对象,既高效又安全。
  6. 提供访问接口:RAII类是资源的“管理者”,而不是资源本身。需要提供方法让外部能够使用被管理的资源,例如提供一个get()方法返回原始句柄,或者重载 ->*运算符。

下面是创建一个RAII类管理C风格的文件句柄的例子:

#include <cstdio>    //管理文件的C函数:FILE、fopen、fclose、fputs
#include <utility>   //引入std::move
#include <stdexcept> //异常处理

class FileGuard
{
private:
    FILE *m_file_handle = nullptr; // 用来存储文件句柄
public:
    // 在构造函数中获取资源,析构函数接收文件名和打开模式作为参数
    explicit FileGuard(const char *filename, const char *mode)
    {
        // 获取资源
        m_file_handle = fopen(filename, mode);
        // 如果fopen失败,抛出异常
        if (m_file_handle == nullptr)
        {
            throw std::runtime_error("Failed to open file: " + std::string(filename));
        }
        printf("FileGuard: File '%s' opened.\n", filename);
    }
    // 在析构函数中释放资源
    ~FileGuard()
    {
        if (m_file_handle != nullptr)
        {
            printf("FileGuard: Closing file.\n");
            fclose(m_file_handle);
            m_file_handle = nullptr; // 将句柄设为nullptr为了防止意外使用
        }
    }
    // 删除拷贝构造函数
    FileGuard(const FileGuard &) = delete;
    // 删除拷贝赋值运算符
    FileGuard &operator=(const FileGuard &) = delete;
    // 实现移动构造函数
    FileGuard(FileGuard &&other) noexcept : m_file_handle(other.m_file_handle)
    {
        // 这里把源对象的析构函数的句柄置为nullptr,源对象析构的时候就不会关闭文件了
        other.m_file_handle = nullptr;
        printf("FileGuard: Ownership moved (move constructor).\n");
    }
    // 实现移动赋值运算符
    FileGuard &operator=(FileGuard &&other) noexcept
    {
        // 防止自我赋值,例如:my_file=std::move(my_file)
        if (this != &other)
        {
            // 释放当前对象可能持有的资源
            if (m_file_handle != nullptr)
            {
                fclose(m_file_handle);
            }
            // 转移资源所有权
            m_file_handle = other.m_file_handle;
            other.m_file_handle = nullptr;
            printf("FileGuard: Ownership moved (move assignment).\n");
        }
        return *this;
    }
    // 提供访问结果,返回文件句柄,便于进行文件操作
    FILE *get() const
    {
        return m_file_handle;
    }
    // 提供一个简单的方法,判断文件是否成功打开
    explicit operator bool() const
    {
        return m_file_handle != nullptr;
    }
};
cpp

参考资料#

C++ RAII 思想机制详解 | 编程指北-计算机学习指南

C++ 中的 RAII 思想
https://liangbm3.site/blog/c-zhong-de-raii-si-xiang
Author liangbm3
Published at 2025年6月30日
Comment seems to stuck. Try to refresh?✨