RAII的实现原理
概念介绍
RAII 可以简单理解为:资源的获取(申请)和对象的初始化绑定,资源的释放和对象的销毁绑定。
- 当创建一个对象时(栈上 / 作用域内),在构造函数中申请资源(比如内存、文件句柄、锁、网络连接);
- 当对象离开作用域(比如函数执行完毕、异常抛出),编译器会自动调用对象的析构函数,在析构函数中释放资源;
- 核心优势:无论程序正常执行还是抛出异常,只要对象的生命周期结束,资源一定会被释放,无需手动调用
free/close等函数。
C++ 中,栈上对象(非 new 创建的对象)的生命周期严格绑定作用域:
- 进入作用域 → 对象构造(调用构造函数);
- 离开作用域(
}结束、return、异常跳出)→ 对象析构(调用析构函数); - 这个过程是编译器自动保证的,不受程序员手动控制,也不会因异常跳过。
下面通过FileGuard管理FILE*和手写Shared_ptr实现RAII,记录我遇到的问题和思考。
实际用途
RAII 的典型应用场景(理解原理后看实际用途)
- 内存管理:
std::unique_ptr/std::shared_ptr(C++11 后的智能指针)是 RAII 的典型实现,构造时接管裸指针,析构时自动delete; - 锁管理:
std::lock_guard/std::unique_lock,构造时加锁,析构时解锁,避免忘记解锁导致死锁; - 文件 / 句柄管理:如上面的
FileGuard,或管理 socket、管道等系统句柄; - 临时资源:比如临时申请的内存、临时占用的硬件资源。
抛出异常的处理
在抛出异常时会执行对象的析构主要依赖的是编译器的栈展开机制(Stack Unwinding),C++ 规定:当异常导致函数退出时,编译器会自动触发「栈展开」,销毁当前作用域内所有栈上对象,且一定会调用它们的析构函数。当然,前提对象内存是在栈上的,是一个栈对象。
栈展开的执行流程(通俗版),当函数中抛出未捕获的异常时:
- 程序立即停止当前函数的顺序执行;
- 编译器从当前函数的栈帧开始,“反向清理” 所有栈上对象(按创建顺序的逆序销毁);
- 每个对象销毁时,强制调用其析构函数;
- 异常继续向上传播(直到被上层
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 类很重要?
- 解决「禁用拷贝」后的灵活性问题:RAII 类必须禁用拷贝(避免资源重复释放),但移动语义能让资源「安全转移」,比如函数返回 RAII 对象、把 RAII 对象放入 STL 容器;
- 高性能:移动只是「指针赋值」,不需要重新打开 / 关闭文件,比「拷贝(先开新文件,再关旧文件)」高效得多;
- 安全:源对象被置空后,其析构函数不会释放资源,彻底避免「重复释放」的核心问题。
手写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("错误");); - 本质:
- 创建「异常对象」(如
runtime_error实例),存储在「异常存储区」(不属于栈 / 堆,由编译器管理); - 暂停当前函数执行,启动「栈展开」流程,向上查找匹配的
catch块。
- 创建「异常对象」(如
当抛出异常后,编译器会从当前函数开始,沿着「调用栈」向上回溯,直到找到匹配的 catch 块,这个过程称为栈展开。
栈展开
栈展开的关键规则:
- 按「调用栈逆序」销毁局部对象(栈对象),调用其析构函数;
- 跳过所有未执行的代码(如
throw后的语句); - 如果展开到
main()仍未找到catch,则触发std::terminate(); - RAII 的核心价值:栈展开时必须调用析构函数,因此资源会被自动释放(只要栈展开完成)。
异常的捕获(catch)
- 语法:
try { 可能抛异常的代码 } catch (异常类型 变量) { 处理逻辑 }; - 匹配规则(优先级从高到低):
- 精确匹配(如
catch (runtime_error& e)匹配throw runtime_error("")); - 基类匹配(如
catch (exception& e)匹配所有继承自exception的异常); - 万能捕获(
catch (...)匹配所有异常);
- 精确匹配(如
- 注意:
- 推荐用「引用」捕获异常(如
catch (exception& e)),避免拷贝异常对象,且能支持多态; catch (...)是「兜底捕获」,通常用于记录日志后重新抛出(throw;),避免吞掉异常。
- 推荐用「引用」捕获异常(如
异常处理的关键特性
noexcept 与异常
noexcept声明函数「不会抛出异常」(如void func() noexcept {});- 如果
noexcept函数内抛出异常,会直接调用std::terminate(),不会触发栈展开; - 用途:移动构造 / 赋值、析构函数(C++11 后析构默认
noexcept),保证这些函数不抛异常,避免栈展开时崩溃。
析构函数与异常
- 析构函数默认
noexcept,如果析构函数抛出异常,会直接触发std::terminate(); - 规则:析构函数中绝对不能抛出异常,如果必须处理异常,应在析构内部捕获并处理(如记录日志),禁止向外抛出。