学习定位:现代 C++ 核心知识。RAII 是现代 C++ 资源管理的根基,必须优先掌握;智能指针、容器、文件流、锁都建立在这个思路上。

RAII 是 C++ 特有的资源管理范式,全称 Resource Acquisition Is Initialization
核心思想:资源的获取在对象的构造函数中完成,资源的释放在对象的析构函数中自动完成
利用栈对象的生命周期自动管理资源,避免手动释放,天然支持异常安全。


1. RAII适用场景

在传统 C/C++ 编程中,资源(内存、文件、锁、套接字等)需要手动获取和释放:

void traditional() {
    int* p = new int[100];          // 获取内存
    FILE* f = fopen("data.txt", "r"); // 获取文件
    // ... 处理逻辑 ...
    delete[] p;                     // 释放内存
    fclose(f);                      // 关闭文件
}

问题

  • 如果 ... 中有 return 或抛异常,后面的释放代码不会执行 → 资源泄漏
  • 代码复杂时容易遗漏某个释放 → 内存泄漏、句柄泄漏。
  • 所有权不明确:谁负责释放?调用者还是被调用者?

2. RAII 的解决方案

  • 构造函数:获取资源(分配内存、打开文件、加锁等)。如果获取失败,可以抛出异常。
  • 析构函数:释放资源(释放内存、关闭文件、解锁等)。析构函数在对象生命周期结束时自动调用(无论正常退出还是栈展开)。
  • 资源与对象的生命周期绑定:对象创建时资源到手,对象销毁时资源释放。
class RAII_IntArray {
    int* data;
public:
    RAII_IntArray(size_t size) : data(new int[size]) {}  // 构造时分配
    ~RAII_IntArray() { delete[] data; }                  // 析构时释放
    // 禁止拷贝(或实现移动语义)
    RAII_IntArray(const RAII_IntArray&) = delete;
    RAII_IntArray& operator=(const RAII_IntArray&) = delete;
};
 
void safe() {
    RAII_IntArray arr(100);   // 自动分配
    // ... 即使这里 return 或抛异常
}   // arr 离开作用域,析构函数自动释放内存

3. 标准库中的 RAII 类

C++ 标准库提供了大量 RAII 类,你应该优先使用它们,而不是自己写。

资源类型RAII 类说明
动态内存std::unique_ptr<T>独占所有权,自动 delete
动态内存std::shared_ptr<T>共享所有权,引用计数自动 delete
动态数组std::vector<T>连续内存,自动扩容与释放
字符串std::string自动管理字符数组
互斥锁std::lock_guard<std::mutex>构造时 lock,析构时 unlock
互斥锁(灵活)std::unique_lock<std::mutex>支持延迟锁定、提前解锁
文件std::fstream构造时打开,析构时关闭
通用资源自定义类可包装 C 风格资源(如 FILE*HANDLE

示例:使用 std::lock_guard 自动管理锁

#include <mutex>
std::mutex mtx;
 
void increment() {
    std::lock_guard<std::mutex> lock(mtx);   // 构造时 lock
    // 共享数据操作(即使抛异常)
}   // 析构时 unlock,绝对不会死锁

示例:使用 std::fstream 自动管理文件

#include <fstream>
void readFile() {
    std::ifstream ifs("data.txt");   // 构造时打开
    // 读取操作...
}   // 析构时自动 close

4. 自定义 RAII 类(以 C 风格资源为例)

当需要管理标准库未封装的资源时(如 FILE*HANDLE、数据库连接),可以自己写 RAII 包装器。

class FileGuard {
    FILE* f;
public:
    // 构造函数:获取资源,失败抛异常
    FileGuard(const char* filename, const char* mode) 
        : f(fopen(filename, mode)) 
    {
        if (!f) {
            throw std::runtime_error("Failed to open file");
        }
    }
 
    // 析构函数:释放资源
    ~FileGuard() {
        if (f) {
            fclose(f);   // 注意:析构函数不应抛异常
        }
    }
 
    // 禁止拷贝(避免多次 fclose)
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
 
    // 允许移动(可选,转移所有权)
    FileGuard(FileGuard&& other) noexcept : f(other.f) {
        other.f = nullptr;
    }
    FileGuard& operator=(FileGuard&& other) noexcept {
        if (this != &other) {
            if (f) fclose(f);
            f = other.f;
            other.f = nullptr;
        }
        return *this;
    }
 
    // 提供访问原始资源的方法(如需要)
    FILE* get() const { return f; }
};
 
void use_file() {
    FileGuard fg("log.txt", "w");
    // 使用 fg.get() 调用 fwrite 等
}   // 自动 fclose,即使中途 return

5. RAII 与异常安全

C++ 异常发生时,栈展开(stack unwinding)会销毁所有局部对象,自动调用它们的析构函数。RAII 利用这一机制保证了即使抛异常,资源也不会泄漏

  • 基本异常安全:没有资源泄漏,程序状态保持一致。
  • 强异常安全:操作失败时,状态回滚到操作前(通常通过 copy-and-swap 实现,依赖 RAII)。
  • 不抛出保证:所有操作都不会抛出异常(析构函数应总是 noexcept)。
void may_throw() {
    FileGuard fg("data.txt", "r");
    // 如果这里抛异常,fg 的析构函数仍会被调用,文件正确关闭
}

关键规则析构函数绝对不要抛出异常(用 noexcept 声明),否则在栈展开过程中抛异常会导致 std::terminate


6. 常见误区

title:误区 1:RAII 只用于内存管理
任何“获取-释放”的资源都可以用 RAII,包括锁、文件、套接字、数据库连接。
title:误区 2:自定义 RAII 类必须实现拷贝语义
大多数资源不可拷贝(独占所有权),应**删除拷贝构造和赋值**,只提供移动语义。
title:误区 3:有了垃圾回收就不需要 RAII
GC 只管理内存,不管理文件、锁等系统资源。RAII 在 C++ 中仍然必需。

7. 最佳实践

  • 优先使用标准库的 RAII 类vectorstringunique_ptrlock_guardfstream)。
  • 如果需要管理 C 风格资源,立即用自定义类包装,不要裸写 fopen/fclose
  • 遵循“三/五法则”:如果定义了析构函数,通常也需要定义或禁用拷贝构造/赋值,以及定义移动构造/赋值。
  • 析构函数标记为 noexcept(现代 C++ 中默认就是,但显式注明更安全)。

8. 总结

核心要点说明
定义构造函数获取资源,析构函数释放资源
优点自动释放,异常安全,避免泄漏
适用资源内存、文件、锁、套接字、数据库连接等
标准库支持unique_ptrshared_ptrlock_guardfstream
自定义要点禁用拷贝,提供移动,析构不抛异常
与 GC 区别RAII 确定性强、无运行时开销、管理任何资源

一句话:RAII 是 C++ 区别于其他语言的独有特性,掌握它就掌握了资源管理的安全之道。现代 C++ 开发中,不应该再手动调用 deletefcloseunlock