C++ RAII

在 C++ 中,RAII 是管理动态内存等资源的一种重要编程思想。其中 RAII 是 “Resource Acquisition Is Initialization(资源获取即初始化)” 的缩写,它并不是某一个具体的语法关键字,而是一套借助栈对象自动管理资源的编码方法。

刚看到这个直译名字,很多小伙伴可能会觉得有点拗口。说白了,它的思路就是把需要手动释放的资源(如堆内存)绑定到一个栈对象上:在栈对象初始化时获取资源,等栈对象离开作用域销毁时,顺带就在析构函数里自动把资源释放掉。

为什么需要 RAII?

如果我们直接使用 new 和 delete 管理堆内存,“申请资源” 和 “释放资源” 这两步是拆开写在业务代码里的。我们先通过 new 申请一块空间,随后还得时刻记着在每一条退出路径上手写 delete:

void processData(bool hasError) {
    int* p = new int(100);

    if (hasError) {
        delete p;    // 退出前手动释放
        return;
    }

    delete p;        // 结束前手动释放
}

一旦函数内部有五六个条件分支,靠人工在每个出口逐一补写 delete 很容易遗漏,只要漏掉一处就会造成内存泄漏。

相比之下,栈上的局部对象就要省心得多:无论函数是正常执行到末尾,还是在中途遇到 return 提前退出,只要离开作用域,编译器都会自动调用它的析构函数。RAII 正是利用这一特性,将资源的申请和释放封装进同一个管理类中,分成以下 2 步自动完成:

  • 构造时获取资源:在管理类的构造函数中申请资源并赋初值,比如通过 new 分配堆内存。
  • 析构时释放资源:在管理类的析构函数中编写释放代码,比如执行 delete。当栈上的管理对象离开作用域时,编译器会自动调用析构函数清理资源。

C++ RAII 的工作原理

为了直观看到这套机制是怎么工作的,下面我们自己动手写一个简单的 IntGuard 管理类:在它的构造函数里通过 new 申请一块存放整数的堆内存,并在它的析构函数里通过 delete 自动释放这块堆内存。

示例 1:自定义 RAII 管理类

#include <iostream>

class IntGuard {
private:
    int* data;

public:
    IntGuard(int val) {
        data = new int(val);
        std::cout << "获取堆内存:" << *data << "\n";
    }

    ~IntGuard() {
        std::cout << "释放堆内存:" << *data << "\n";
        delete data;
    }

    int getValue() const {
        return *data;
    }
};

void checkScore(int val, bool stopEarly) {
    IntGuard guard(val);

    if (stopEarly) {
        std::cout << "函数提前退出\n";
        return;
    }

    std::cout << "当前分数:" << guard.getValue() << "\n";
}

int main() {
    checkScore(85, true);
    checkScore(96, false);
    return 0;
}

运行结果如下。

获取堆内存:85
函数提前退出
释放堆内存:85
获取堆内存:96
当前分数:96
释放堆内存:96

分析:

在这个例子中,业务函数 checkScore() 内部没有写任何一句 delete,只是定义了一个栈上的局部管理对象 guard,但两次调用都把堆内存释放干净了。

第一次调用传入 true 时,程序进入 if (stopEarly) 分支提前 return。就在离开函数的瞬间,栈对象 guard 自动触发析构函数 ~IntGuard(),执行里面的 delete data 并输出 “释放堆内存:85”。

第二次调用传入 false 时,函数正常执行到末尾的右花括号 },guard 同样自动销毁并释放堆内存,输出 “释放堆内存:96”。

可能有些小伙伴会问:“IntGuard 类里面不还是写了 new 和 delete 吗?这怎么就省事了呢?”

其实道理很简单:new 和 delete 只需要在编写 IntGuard 管理类时集中写一次。封装好之后,我们在其他业务函数里直接把 IntGuard 当成普通栈对象来使用就行了,哪怕中途 return 再多次,也不用再手写一句 delete。

那如果我们不想等到整个函数结束,而是希望在函数执行到一半时就提前把这块资源释放掉,应该怎么做呢?其实很简单,因为栈对象的生命周期严格跟随代码块,只需要单独加一对花括号 {} 把它包起来即可。请看下面例子。

示例 2:利用代码块提前释放

#include <iostream>

class IntGuard {
private:
    int* data;

public:
    IntGuard(int val) {
        data = new int(val);
        std::cout << "获取堆内存:" << *data << "\n";
    }

    ~IntGuard() {
        std::cout << "释放堆内存:" << *data << "\n";
        delete data;
    }

    int getValue() const {
        return *data;
    }
};

int main() {
    std::cout << "1. 进入函数\n";

    {
        IntGuard temp(50);
        std::cout << "临时数据:" << temp.getValue() << "\n";
    }    // 离开代码块立即释放

    std::cout << "2. 继续执行后续任务\n";
    return 0;
}

运行结果如下。

1. 进入函数
获取堆内存:50
临时数据:50
释放堆内存:50
2. 继续执行后续任务

分析:

从输出顺序可以看出,“释放堆内存:50” 出现在了 “2. 继续执行后续任务” 之前。因为 temp 定义在内部那对独立的花括号 {} 中,一旦走出内层右花括号 },temp 的作用域就结束了,它管理的堆内存也会立即释放。

提示 上面的 IntGuard 类仅用于演示 RAII 的基本原理,还没有处理对象复制等细节。在实际开发中,我们一般不需要自己手写这类底层内存包装类,直接使用 C++ 标准库现成的 RAII 工具即可。

标准库中的常用 RAII 类型

在 C++ 标准库中,常见的动态资源都已经封装成了现成的 RAII 类型,主要包括以下 3 类:

  • 动态容器与字符串:比如 std::vector 和 std::string。它们内部其实也是在堆上动态分配内存来存放数据的,但因为本身就是标准的 RAII 类,在析构函数中会自动释放内部申请的堆内存。
  • C++ 智能指针:比如 <memory> 头文件提供的 std::unique_ptr 和 std::shared_ptr。它们专门用来包装动态创建的单个堆对象,离开作用域时会自动销毁所管理的堆对象,这也是接下来学习的重点。
  • 文件流与互斥锁管理器:比如文件操作中的 std::ifstream、多线程编程中的 std::lock_guard,后面章节会陆续接触到。当这些对象离开作用域销毁时,会自动关闭打开的文件或解开互斥锁,避免忘记收尾。

下面我们以 std::string 和 std::vector 为例,看看标准库中的 RAII 类型是如何帮我们省去手动释放工作的。

示例 3:使用标准库 RAII 容器

#include <iostream>
#include <string>
#include <vector>

void loadCourse(bool cancel) {
    std::string site = "绿叶网";
    std::vector<int> scores = {88, 92, 95};

    if (cancel) {
        std::cout << site << ":任务取消,提前返回\n";
        return;    // 自动清理内部堆内存
    }

    std::cout << site << ":共有 " << scores.size() << " 条成绩记录\n";
}

int main() {
    loadCourse(true);
    loadCourse(false);
    return 0;
}

运行结果如下。

绿叶网:任务取消,提前返回
绿叶网:共有 3 条成绩记录

分析:

在 loadCourse() 函数中,局部对象 site 和 scores 本身存放在栈上,但它们内部用来保存字符和数组元素的空间都是在堆内存上动态分配的。

因为 std::string 和 std::vector 内部已经按照 RAII 思想写好了析构函数,所以哪怕程序在 if (cancel) 分支直接 return 提前退出,它们占用的堆内存也会自动清理干净,不需要我们手写任何释放代码。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号