RAII的实现原理


RAII的实现原理

概念介绍

RAII 可以简单理解为:资源的获取(申请)和对象的初始化绑定,资源的释放和对象的销毁绑定

  • 当创建一个对象时(栈上 / 作用域内),在构造函数中申请资源(比如内存、文件句柄、锁、网络连接);
  • 当对象离开作用域(比如函数执行完毕、异常抛出),编译器会自动调用对象的析构函数,在析构函数中释放资源;
  • 核心优势:无论程序正常执行还是抛出异常,只要对象的生命周期结束,资源一定会被释放,无需手动调用 free/close 等函数。

C++ 中,栈上对象(非 new 创建的对象)的生命周期严格绑定作用域:

  • 进入作用域 → 对象构造(调用构造函数);
  • 离开作用域(} 结束、return、异常跳出)→ 对象析构(调用析构函数);
  • 这个过程是编译器自动保证的,不受程序员手动控制,也不会因异常跳过。

下面通过FileGuard管理FILE*和手写Shared_ptr实现RAII,记录我遇到的问题和思考。

实际用途

RAII 的典型应用场景(理解原理后看实际用途)

  1. 内存管理std::unique_ptr/std::shared_ptr(C++11 后的智能指针)是 RAII 的典型实现,构造时接管裸指针,析构时自动 delete
  2. 锁管理std::lock_guard/std::unique_lock,构造时加锁,析构时解锁,避免忘记解锁导致死锁;
  3. 文件 / 句柄管理:如上面的 FileGuard,或管理 socket、管道等系统句柄;
  4. 临时资源:比如临时申请的内存、临时占用的硬件资源。

抛出异常的处理

在抛出异常时会执行对象的析构主要依赖的是编译器的栈展开机制(Stack Unwinding),C++ 规定:当异常导致函数退出时,编译器会自动触发「栈展开」,销毁当前作用域内所有栈上对象,且一定会调用它们的析构函数。当然,前提对象内存是在栈上的,是一个栈对象。

栈展开的执行流程(通俗版),当函数中抛出未捕获的异常时:

  1. 程序立即停止当前函数的顺序执行;
  2. 编译器从当前函数的栈帧开始,“反向清理” 所有栈上对象(按创建顺序的逆序销毁);
  3. 每个对象销毁时,强制调用其析构函数;
  4. 异常继续向上传播(直到被上层 catch 捕获,或程序终止)。

手动管理资源抛出异常的代码

通过一个典型的错误示例,理解手动管理的问题所在:

#include <iostream>
#include <cstdio>
using namespace std;

void manualResource() {
    FILE* file = fopen("test.txt", "w");
    if (file == nullptr) {
        return;
    }
    try {
        throw runtime_error("模拟业务异常");
    } catch (...) {
        throw;
    }

    fclose(file); 
    cout << "文件已手动关闭" << endl;
}

int main() {
    try {
        manualResource();
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    return 0;
}

手动管理资源时,释放代码是顺序执行的:只有前面的代码正常执行完,才会执行fclose。一旦中间抛出异常且未在当前作用域捕获,程序会直接跳出函数,释放步骤被 “跳过”,资源就泄露了。

RAII资源管理代码示例

RAII 的关键不是 “写了析构函数”,而是 C++ 规定:当异常导致函数退出时,编译器会自动触发「栈展开」,销毁当前作用域内所有栈上对象,且一定会调用它们的析构函数

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

class FileGuard {
public:
    explicit FileGuard(const char* filename, const char* mode) {
        m_file = fopen(filename, mode);
        if (m_file == nullptr) {
            throw runtime_error(string("文件打开失败: ") + strerror(errno) + " (" + filename + ")");
        }
        cout << "文件已打开:" << filename << endl;
    }

    ~FileGuard() {
        if (m_file != nullptr) {
            fclose(m_file);
            cout << "文件已关闭" << endl;
        }
    }

    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
    
    FileGuard(FileGuard&& other) noexcept {
        m_file = other.m_file;
        other.m_file = nullptr;
    }
    FileGuard& operator=(FileGuard&& other) noexcept {
        if (this != &other) {
            if (m_file != nullptr) fclose(m_file);
            m_file = other.m_file;
            other.m_file = nullptr;
        }
        return *this;
    }

    FILE* get() const { return m_file; }

private:
    FILE* m_file;
};

void testRAII() {
    try {
        FileGuard fg("test.txt", "w");
        fprintf(fg.get(), "Hello RAII!\n");
        cout << "内容已写入文件" << endl;
        
        throw runtime_error("主动抛出的测试异常");
    } catch (const exception& e) {
        cerr << "捕获到异常: " << e.what() << endl;
    }
}

void wrongRAII() {
    try {
        FileGuard* fg = new FileGuard("test.txt", "w"); // 堆对象
        throw runtime_error("主动抛出的测试异常");
        delete fg; // 永远执行不到
    } catch (const exception& e) {
        cerr << "wrongRAII 捕获到异常: " << e.what() << endl;
    }
}

int main() {
    testRAII();
    return 0;
}

FileGuard 类禁用了拷贝和赋值构造函数,这是为了防止拷贝出现的两个对象同时管理一个文件指针,导致重复关闭文件。但是也出现了一个问题,无法把FileGuard对象资源转移给另一个对象。

假设现在没有实现移动构造函数和移动赋值运算符,以下示例代码:

FileGuard createFile(const char* filename) {
    FileGuard f = FileGuard(filename, "w"); // 创建栈对象
    return f; // 编译报错:尝试调用被delete的拷贝构造
}

报错信息:

<source>: In function 'FileGuard createFile(const char*)':
<source>:43:12: error: use of deleted function 'FileGuard::FileGuard(const FileGuard&)'
   43 |     return f;
      |            ^
<source>:17:5: note: declared here
   17 |     FileGuard(const FileGuard& other) = delete;
      |     ^~~~~~~~~
<source>:43:12: note: use '-fdiagnostics-all-candidates' to display considered candidates
   43 |     return f;
      |            ^
Compiler returned: 1

拷贝构造函数的调用时机

(1)显式 / 隐式拷贝对象(FileGuard fg2 = fg1; // 直接拷贝初始化 → 调用拷贝构造;FileGuard fg3(fg1); // 显式调用拷贝构造)

(2)函数参数按值传递

(3)函数返回局部对象(无 NRVO 优化时)

补充:NRVO(命名返回值优化)是编译器的优化手段,会直接在返回值空间创建对象,跳过拷贝 / 移动,但标准要求「拷贝 / 移动构造必须可访问」,所以即使有 NRVO,禁用拷贝且无移动仍会编译报错。

代码:https://godbolt.org/z/1q7Gz65sT

wrongRAII函数中,new 创建的是堆对象,C++ 不会自动销毁堆对象,抛异常后,delete fg 永远执行不到 → 析构函数不会被调用 → 文件不会关闭 → 资源泄露。

移动构造函数

FileGuard(FileGuard&& other) {
    m_file = other.m_file;
    other.m_file = nullptr;
}

逐行解释

  • FileGuard&& other&& 是「右值引用」,专门用来绑定「临时对象」(比如函数返回的对象、匿名对象),表示这个对象是「可以被移动的」;
  • noexcept:声明这个函数不会抛出异常(移动资源只是指针赋值,无风险),STL 容器(如 vector)会优先选择 noexcept 的移动构造,提升性能;
  • m_file = other.m_file:把源对象 other 管理的文件指针「拿过来」,新对象成为资源的新主人;
  • other.m_file = nullptr:把源对象的指针置空后,当源对象(临时对象)析构时,析构函数看到 m_file == nullptr,就不会调用 fclose,避免资源被重复释放。每次完成这种资源转移的函数时,都需要考虑other和this分别如何处理。

有了移动构造:return fg 会把 fg 的资源转移给返回的临时对象,再转移给 myFile,全程只有一次 fopen 和一次 fclose,安全且高效。

移动赋值函数

FileGuard& operator=(FileGuard&& other) {
    if(other != *this) {
        if(m_file != nullptr) {
            fclose(m_file);
            m_file = nullptr;
        }
        m_file = other.m_file;
        other.m_file = nullptr;
    }
}

逐行解释

  • 场景:针对「已存在的对象」赋值(比如 fg1 = std::move(fg2)),和移动构造的区别是「新对象已经创建,可能已有资源」;
  • this != &other:防止自赋值(比如 fg = std::move(fg)),如果不判断,会先 fclose(this->m_file),再去拿 other.m_file(已经被自己关了),导致野指针;
  • if (m_file != nullptr) fclose(m_file):因为赋值的目标对象(this)可能已经管理着一个打开的文件,先关闭它,再接管新资源,避免资源泄漏;
  • 后续逻辑和移动构造一致:接管资源 + 源对象置空。

为什么这两个函数对 RAII 类很重要?

  1. 解决「禁用拷贝」后的灵活性问题:RAII 类必须禁用拷贝(避免资源重复释放),但移动语义能让资源「安全转移」,比如函数返回 RAII 对象、把 RAII 对象放入 STL 容器;
  2. 高性能:移动只是「指针赋值」,不需要重新打开 / 关闭文件,比「拷贝(先开新文件,再关旧文件)」高效得多;
  3. 安全:源对象被置空后,其析构函数不会释放资源,彻底避免「重复释放」的核心问题。

手写Shared_ptr管理资源

源码中的结构

shared_ptr继承至__shared_ptr__shared_ptr管理了两个成员,_M_ptr_M_refcount_M_ptr是由智能指针管理的普通指针,_M_refcount是一个引用计数器,类型是__shared_count__shared_count中管理了_M_pi,是一个计数器,类型是_Sp_counted_base

__shared_count内部的拷贝构造函数和赋值运算符都调用了_M_add_ref_copy方法,来增加引用计数,析构函数调用了_M_pi_M_release方法,释放了_M_pi的内存。__shared_count中的方法都是借助_M_pi实现的,所以继续看_Sp_counted_base的实现。

_Sp_counted_base中有两个类成员,_M_use_count(引用计数)和_M_weak_count(弱引用计数)。_M_release方法是该类的关键,其中的_M_add_red_copy_M_weak_add_red分别处理这两个原子计数的自增过程。

_Sp_counted_ptr_Sp_count_base的派生类,并且__shared_count在初始化_M_pi是也是使用了_Sp_counted_ptr。其中实现的_M_dispose才是删除了shared_ptr管理的指针(内存块),_M_destroy用于释放自己的内存。

代码

#include <iostream>
#include <stdexcept>
#include <utility> // 用于 std::swap、std::move
using namespace std;

// 简化版 SharedPtr 实现(模板类,支持任意类型)
template <typename T>
class SharedPtr {
public:
    // ========== 构造函数 ==========
    // 1. 空构造:管理空资源,引用计数为 nullptr
    SharedPtr() noexcept : m_ptr(nullptr), m_refCount(nullptr) {}

    // 2. 普通构造:接管裸指针,初始化引用计数为 1
    explicit SharedPtr(T* ptr) : m_ptr(ptr) {
        if (ptr != nullptr) {
            // 引用计数存在堆上,保证所有共享对象能访问同一个计数
            m_refCount = new size_t(1);
            cout << "资源初始化,引用计数 = " << *m_refCount << endl;
        } else {
            m_refCount = nullptr;
        }
    }

    // ========== 拷贝构造:共享资源,引用计数+1 ==========
    SharedPtr(const SharedPtr& other) noexcept {
        // 共享对方的资源和引用计数
        m_ptr = other.m_ptr;
        m_refCount = other.m_refCount;

        // 如果不是空资源,引用计数+1
        if (m_refCount != nullptr) {
            (*m_refCount)++;
            cout << "拷贝构造,引用计数 = " << *m_refCount << endl;
        }
    }

    // ========== 拷贝赋值:先释放当前资源,再共享新资源 ==========
    SharedPtr& operator=(const SharedPtr& other) noexcept {
        // 防止自赋值(自己赋值给自己)
        if (this == &other) {
            return *this;
        }

        // 第一步:释放当前对象持有的资源(引用计数-1,为0则销毁资源)
        release();

        // 第二步:共享对方的资源,引用计数+1
        m_ptr = other.m_ptr;
        m_refCount = other.m_refCount;
        if (m_refCount != nullptr) {
            (*m_refCount)++;
            cout << "拷贝赋值,引用计数 = " << *m_refCount << endl;
        }

        return *this;
    }

    // ========== 移动构造:接管资源,原对象置空(零成本) ==========
    SharedPtr(SharedPtr&& other) noexcept {
        // 接管对方的资源和引用计数
        m_ptr = other.m_ptr;
        m_refCount = other.m_refCount;

        // 原对象置空,避免其析构时释放资源
        other.m_ptr = nullptr;
        other.m_refCount = nullptr;
        cout << "移动构造,资源所有权转移" << endl;
    }

    // ========== 移动赋值:接管资源,原对象置空 ==========
    SharedPtr& operator=(SharedPtr&& other) noexcept {
        if (this == &other) {
            return *this;
        }

        // 先释放当前资源
        release();

        // 接管对方资源
        m_ptr = other.m_ptr;
        m_refCount = other.m_refCount;

        // 原对象置空
        other.m_ptr = nullptr;
        other.m_refCount = nullptr;
        cout << "移动赋值,资源所有权转移" << endl;

        return *this;
    }

    // ========== 析构函数:引用计数-1,为0则释放资源 ==========
    ~SharedPtr() noexcept {
        release();
        cout << "析构函数执行,资源已清理(若计数为0)" << endl;
    }

    // ========== 核心接口 ==========
    // 获取资源指针(解引用前检查空指针)
    T* get() const noexcept {
        return m_ptr;
    }

    // 解引用(重载*)
    T& operator*() const {
        if (m_ptr == nullptr) {
            throw runtime_error("解引用空的 SharedPtr");
        }
        return *m_ptr;
    }

    // 成员访问(重载->)
    T* operator->() const {
        if (m_ptr == nullptr) {
            throw runtime_error("访问空的 SharedPtr 的成员");
        }
        return m_ptr;
    }

    // 获取当前引用计数
    size_t use_count() const noexcept {
        return m_refCount ? *m_refCount : 0;
    }

    // 判断是否独占资源(计数为1)
    bool unique() const noexcept {
        return use_count() == 1;
    }

    // 重置资源(释放当前资源,接管新资源)
    void reset(T* ptr = nullptr) noexcept {
        // 先释放当前资源
        release();

        // 接管新资源
        m_ptr = ptr;
        if (ptr != nullptr) {
            m_refCount = new size_t(1);
            cout << "重置资源,引用计数 = " << *m_refCount << endl;
        } else {
            m_refCount = nullptr;
        }
    }

private:
    // 核心:释放资源的逻辑(封装为私有函数,避免重复代码)
    void release() noexcept {
        // 1. 如果是空资源,直接返回
        if (m_refCount == nullptr) {
            return;
        }

        // 2. 引用计数-1
        (*m_refCount)--;
        cout << "引用计数-1,当前计数 = " << *m_refCount << endl;

        // 3. 计数为0时,销毁资源和引用计数
        if (*m_refCount == 0) {
            delete m_ptr;       // 释放管理的资源(RAII 核心:析构释放)
            delete m_refCount;  // 释放引用计数本身
            cout << "资源已彻底释放(计数为0)" << endl;
        }

        // 4. 置空,避免野指针
        m_ptr = nullptr;
        m_refCount = nullptr;
    }

    T* m_ptr;         // 管理的资源指针(RAII 管理的核心资源)
    size_t* m_refCount; // 引用计数指针(堆上分配,共享计数)
};

// ========== 测试代码:验证 RAII 和引用计数 ==========
// 测试普通资源(int)
void testIntResource() {
    cout << "===== 测试 int 资源管理 =====" << endl;
    // 构造:接管资源,计数=1
    SharedPtr<int> p1(new int(10));
    cout << "p1 指向的值:" << *p1 << ",计数:" << p1.use_count() << endl;

    // 拷贝构造:计数+1=2
    SharedPtr<int> p2 = p1;
    cout << "p2 指向的值:" << *p2 << ",计数:" << p2.use_count() << endl;

    // 拷贝赋值:p3 共享资源,计数+1=3
    SharedPtr<int> p3;
    p3 = p2;
    cout << "p3 指向的值:" << *p3 << ",计数:" << p3.use_count() << endl;

    // 重置 p1:释放当前资源(计数-1=2),接管新资源
    p1.reset(new int(20));
    cout << "p1 重置后的值:" << *p1 << ",计数:" << p1.use_count() << endl;
    cout << "p2 计数:" << p2.use_count() << endl; // 仍为2

    // 移动构造:p4 接管 p1 的资源,p1 置空
    SharedPtr<int> p4 = move(p1);
    if (p1.get() == nullptr) {
        cout << "p1 移动后为空,p4 值:" << *p4 << ",计数:" << p4.use_count() << endl;
    }
}

// 测试 RAII 管理文件资源(结合之前的 FileGuard 思想)
struct FileResource {
    FILE* m_file;
    // 构造:获取资源(打开文件)
    FileResource(const char* filename, const char* mode) {
        m_file = fopen(filename, "w");
        if (m_file == nullptr) {
            throw runtime_error("文件打开失败");
        }
        cout << "文件已打开:" << filename << endl;
    }
    // 析构:释放资源(关闭文件)
    ~FileResource() {
        if (m_file != nullptr) {
            fclose(m_file);
            cout << "文件已关闭" << endl;
        }
    }
    // 写入内容
    void write(const char* content) {
        fprintf(m_file, "%s", content);
    }
};

void testFileResource() {
    cout << "\n===== 测试文件资源管理(RAII + SharedPtr) =====" << endl;
    try {
        // SharedPtr 管理 FileResource(RAII 嵌套)
        SharedPtr<FileResource> fp1(new FileResource("test.txt", "w"));
        fp1->write("Hello SharedPtr RAII!");
        cout << "fp1 计数:" << fp1.use_count() << endl;

        // 共享文件资源
        SharedPtr<FileResource> fp2 = fp1;
        cout << "fp2 计数:" << fp2.use_count() << endl;

        // 抛出异常,验证 RAII 仍生效
        throw runtime_error("测试异常");
    } catch (const exception& e) {
        cerr << "捕获异常:" << e.what() << endl;
    }
    // 函数结束后,fp1/fp2 析构,计数减为0 → FileResource 析构 → 文件关闭
}

int main() {
    testIntResource();
    testFileResource();
    return 0;
}

异常处理机制

异常处理的核心是「抛出(throw)- 捕获(catch)- 栈展开(stack unwinding)」的闭环,从底层逻辑拆解:

异常的抛出(throw)

  • 语法:throw 表达式;(如 throw runtime_error("错误"););
  • 本质:
    1. 创建「异常对象」(如 runtime_error 实例),存储在「异常存储区」(不属于栈 / 堆,由编译器管理);
    2. 暂停当前函数执行,启动「栈展开」流程,向上查找匹配的 catch 块。

当抛出异常后,编译器会从当前函数开始,沿着「调用栈」向上回溯,直到找到匹配的 catch 块,这个过程称为栈展开

栈展开

栈展开的关键规则:

  1. 按「调用栈逆序」销毁局部对象(栈对象),调用其析构函数;
  2. 跳过所有未执行的代码(如 throw 后的语句);
  3. 如果展开到 main() 仍未找到 catch,则触发 std::terminate()
  4. RAII 的核心价值:栈展开时必须调用析构函数,因此资源会被自动释放(只要栈展开完成)。

异常的捕获(catch)

  • 语法:try { 可能抛异常的代码 } catch (异常类型 变量) { 处理逻辑 }
  • 匹配规则(优先级从高到低):
    1. 精确匹配(如 catch (runtime_error& e) 匹配 throw runtime_error(""));
    2. 基类匹配(如 catch (exception& e) 匹配所有继承自 exception 的异常);
    3. 万能捕获(catch (...) 匹配所有异常);
  • 注意:
    • 推荐用「引用」捕获异常(如 catch (exception& e)),避免拷贝异常对象,且能支持多态;
    • catch (...) 是「兜底捕获」,通常用于记录日志后重新抛出(throw;),避免吞掉异常。

异常处理的关键特性

noexcept 与异常

  • noexcept 声明函数「不会抛出异常」(如 void func() noexcept {});
  • 如果 noexcept 函数内抛出异常,会直接调用 std::terminate()不会触发栈展开
  • 用途:移动构造 / 赋值、析构函数(C++11 后析构默认 noexcept),保证这些函数不抛异常,避免栈展开时崩溃。

析构函数与异常

  • 析构函数默认 noexcept,如果析构函数抛出异常,会直接触发 std::terminate()
  • 规则:析构函数中绝对不能抛出异常,如果必须处理异常,应在析构内部捕获并处理(如记录日志),禁止向外抛出。

文章作者: AllenMirac
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AllenMirac !
  目录