news 2026/7/2 1:45:54

RAII 有什么作用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAII 有什么作用

RAII 的作用主要体现在:自动资源管理异常安全简化代码提高可维护性

自动资源管理 获取资源后交由 RAII 类保管,离开作用域后资源被妥善释放,减少手动资源管理容易出现的忘记释放和重复释放。

异常安全 代码可能在任何步骤抛出异常,C++ 保证在异常发生后,已经完全构造的局部变量会被析构,所以如果资源被一个已经构造好的 RAII 类保存着,那么在异常发生后它就能被安全释放。

简化代码 在复杂逻辑,特别是多返回路径的函数中,使用 RAII 类管理资源或状态,可大大降低手动管理带来的复杂性,增强可读性。

提高可维护性 RAII 类封装了资源管理的细节,与其他逻辑分离,便于代码维护。

RAII 类的工作原理

RAII 类依赖于 C++ 的栈对象生命周期管理机制,通过定义构造、拷贝和析构函数来精确控制类在创建、复制和销毁时的行为,以实现核心的资源保存、流转和释放。

构造函数 构造函数接受资源,将其存储在类中,同时初始化相关状态或接受其他与资源管理相关联的数据。比如 std::shared_ptr 除了存储指针外,还存储该指针的引用计数,在构造时必须初始化引用计数,它还支持传入自定义的删除器(我的上一篇随笔C++ 智能指针的删除器对它作过讨论)。

拷贝和移动函数 包括拷贝构造、移动构造、拷贝赋值、移动赋值四个成员函数,它们共同描述了资源的转移行为。

当资源为独占时,就不能允许发生复制动作,那么拷贝构造和拷贝赋值函数应该定义为删除,但是从一个临时的 RAII 类接管资源很合理,所以需要定义它的移动构造和移动赋值函数。一个现成的例子就是 std::unique_ptr:

代码

std::unique_ptr<int> create_unique(int value) { std::unique_ptr<int> ret(new int(value)); return ret;//可能触发NRVO } std::unique_ptr<int> piu1(new int(42)); std::unique_ptr<int> piu2 = piu1;//错误,无法拷贝构造 std::unique_ptr<int> piu3; piu3 = piu1;//错误,无法拷贝赋值 piu3 = create_unique(42);//可以,接管指针 piu3 = std::move(piu1);//强行转移所有权 piu3.reset(piu1.release());//使用unique_ptr提供的接口强行转移所有权

上述代码提到 NRVO(Named Return Value Optimization,具名返回值优化)是 C++ 拷贝消除机制(Copy Elision)的一种具体形式,该机制旨在消除不必要的临时对象拷贝以提高程序性能,可到 cppreference:copy_elision 查看详细讲解。
示例代码中的 create_unique 返回一个名为 ret 的局部变量,并且没有其他引用绑定到 ret 上,如果这样调用create_unique:std::unique_ptr<int> piu4 = create_unique(42);在编译器支持 NRVO 的情况下,ret 变量不会被实际创建,而是直接在外部 piu4 的内存位置直接构造,达到消除拷贝的目的。
若编译器未支持或者代码情况不满足 NRVO 条件,移动构造则作为第二候选用来避免拷贝,拷贝构造的优先级最低,因为拷贝一个对象可能付出高昂的代价。

由于 std::unique_ptr 删除了拷贝构造和拷贝赋值函数,我们无法复制一个现有的实例;但是定义了移动构造和移动赋值函数,我们可以在函数中返回一个局部构造的实例,用以构造或者赋值给另一个 std::unique_ptr。强行转移 std::unique_ptr 的资源所有权是可以的,但是为了宣示独占性,手动转移的语法都不那么自然。

而当资源能够共享时,除了定义移动构造函数和移动赋值函数用以接管临时对象资源外,拷贝构造和拷贝赋值函数的定义显得更为重要。std::shared_ptr 的拷贝函数维护引用计数,这是它实现指针管理的重要一环;而容器类如 std::vector 的拷贝函数,需要负责可能的内存清理和分配,所有元素的拷贝,以及过程中异常的处理。

析构函数 析构函数负责资源的清理工作,意味着一个实例工作的结束,但是要避免让异常逃离析构函数(Scott Meyers, Effective C++, Item 8)。

上述函数的主要职责是确保 RAII 类与编译器的协作,实现资源的自动生命周期管理,而为了使资源管理更加灵活,RAII 类通常还会提供一系列面向用户的接口,这些接口依据具体资源的特性设计,用以支持资源的读取、修改或状态查询,兼顾自动化与可操作性。这使得 RAII 类成为底层机制与上层接口之间的桥梁,保证精细复杂的资源操作以稳定可靠的方式进行。

使用标准库的 RAII 设施

标准库提供的诸多常用设施都是典型的 RAII 思想践行者,且都是精工细作,历经千锤百炼的,使用它们可以使绝大部分的资源管理变得自然而简洁。

容器类 大部分标准库容器都需要申请和释放动态内存,而这些工作都被标准库的实现者隐藏于表面之下,阻隔了手动管理动态内存的危险:

代码

std::vector<int> veci; veci.reserve(1);//预先申请动态内存 veci.push_back(0); veci.push_back(1);//内存不够用了,自动重新申请动态内存并迁移数据 veci.clear();//清理数据和内存

文件流类 使用标准库的文件流类,而不是直接使用 FILE *, 那么就不用担心有个打开的文件在不使用之后忘记关闭了。

锁管理类 使用标准库的锁管理类在进入临界区时锁定互斥量,那么一个提前离开临界区的动作就不会导致互斥量的解锁被跳过了:

代码

static std::mutex mutex1, mutex2, mutex3; void syncOperation() { //C++17之前,先同时锁定三个互斥量,然后用lock_guard领养它们 std::lock(mutex1, mutex2, mutex3); std::lock_guard<std::mutex> guard1(mutex1, std::adopt_lock); std::lock_guard<std::mutex> guard2(mutex2, std::adopt_lock); std::lock_guard<std::mutex> guard3(mutex3, std::adopt_lock); //不好的锁定方式,若其他线程以不同的顺序锁定互斥量,极易造成死锁 //std::lock_guard<std::mutex> guard1(mutex1); //std::lock_guard<std::mutex> guard2(mutex2); //std::lock_guard<std::mutex> guard3(mutex3); //C++17之后,使用scoped_lock std::scoped_lock lock(mutex1, mutex2, mutex3); ...//后续操作无论无论在何处返回,或者抛出异常,三个互斥量都保证能被解锁 }

智能指针类 使用标准库的智能指针管理指针,那么当无人引用该指针后,它所指涉的资源就能被及时释放:

代码

std::shared_ptr<int> create_shared(int value) { std::shared_ptr<int> ret(new int(value)); return ret; } //函数内创建的指针被智能指针接管 auto pis1 = create_shared(42); //资源在智能指针之间流转 auto pis2 = pis1; pis1.release(); std::shared_ptr<int> pis3(pis2); ... //最后一个持有资源的智能指针析构时释放资源

创建自己的 RAII 类

标准库的 RAII 设施兼顾通用性和高性能,在设计上都极端考究,并且已经可以满足绝大部分的日常需求了,我们通常没有必要去构建与标准库类似的复杂设施(如果你有,那么能读到这里我实在受宠若惊),但是将 RAII 思想应用到日常的编码中,也能给我们带来诸多益处,在此我抛出几块拙劣的砖用以举例。

值同步

假如我们在调试一个函数时,需要将某个值改变为一个临时的测试值,但是函数结束后,这个值需要被还原为它初始的值,不能影响后续的程序执行:

代码

template<typename Op, typename Tar = Op> class ValueSynchronizer { public: ValueSynchronizer(Op &operand, Tar target) : _operand(operand), _target(target){ } ~ValueSynchronizer(){ _operand = _target; } private: _Op &_operand; const Tar _target; }; //在调试时使用(假如debug_value是一个全局变量或foo所属类的一个成员变量) void foo() { //创建debug_value的一个快照 ValueSynchronizer<int> vs(debug_value, debug_value); //后续的调试操作修改debug_value }

现在无论 foo() 的逻辑多么复杂,在它返回时 debug_value 一定会还原到函数进入时的数值。

ValueSynchronizer 的设计还能让它做其他一些事情,比如有一个设置值的函数,它需要将目标变量设置为传入的新值,但是在离开函数之前,旧值可能还会被使用,那么我们可以这样编写这个函数:

代码

//假如_value是一个全局变量或者set_value所属类的一个成员变量 void set_value(int new_value) { ValueSynchronizer<int> vs(_value, new_value); //其他的一些可能还会用到_value旧值的逻辑 if(_value == 0) return; ... }

ValueSynchronizer 的作用可以概括为:在创建时为操作对象指定一个目标值,保证在离开作用域后,该操作对象同步到设置的目标值。

过程计时器

RAII 类将资源与类的生命周期绑定的特性,很容易让人联想到一种过程计时器的实现:

代码

class ScopedTimer { public: explicit ScopedTimer(const std::string &scope_name) : _start(clock()), _scope_name(scope_name){ } ~ScopedTimer() { std::cout<< _scope_name << " duration: " << (clock() - _start)/(float)CLOCKS_PER_SEC << " seconds.\n"; } private: clock_t _start; const std::string _scope_name; }; //使用过程计时器 void foo() { ScopedTimer function_timer(__func__); { ScopedTimer block_timer("inner block"); ... } ... }

这非常适用于函数调用或者一个代码块的耗时统计,不用在作用域的开始和结束位置分别插入时间统计的代码,有利于维持代码的整洁可读。

临时目录管理

数据处理类代码对临时目录的管理也非常契合 RAII 思想,在开始处理前创建临时目录,处理过程中写入临时数据,过程结束后需要删除临时目录:

代码

class TemporaryDirectory { public: explicit TemporaryDirectory(const std::string &dir): _dir(dir) { create_directory(_dir); } ~TemporaryDirectory() { //目录存在则将其删除 if(directory_exist(_dir) == true) remove_directory(_dir); } bool valid() const { return directory_exist(_dir); } private: std::string _dir; }; void process_data() { TemporaryDirectory td(temp); if(td.valid() == false) { std::cerr<< "can't create temporary directory, abort.\n" return; } ...//处理数据 }

目前为止,前面理论部分强调的拷贝和移动函数我们都没有关注过,因为这些例子使用场景非常简单,暂不触及它们。如果代码中需要它们(无论是直接的还是间接的),而我们又没有定义时,编译器就会按需合成他们的默认版本,具体的合成规则会深远影响到代码的行为。

我们扩展一下 TemporaryDirectory 的使用场景,以说明这一影响。假如我们的函数接受一个临时目录列表,需要创建好这些临时目录后再开展工作,我们这样编写代码:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 1:45:45

后台状态巡检低效怎么排查:状态字段、截图证据和任务日志设计

很多团队都有一类重复任务&#xff1a; 每天打开多个后台页面&#xff0c;检查状态是否正常&#xff0c;确认有没有异常提示&#xff0c;再把结果发到群里或写进表格。 刚开始&#xff0c;这种方式能跑起来。 但后台数量变多、参与人员变多、检查频率变高以后&#xff0c;问…

作者头像 李华
网站建设 2026/7/2 1:42:35

电子自旋的诡异之谜破解 —— 原创电子结构理

百年量子难题&#xff1a;电子自旋始终是物理学 “悬案” 从反常塞曼效应被发现至今&#xff0c;电子自旋已经困扰物理学界百年。现有量子力学体系仅将1/2 自旋简单定义为电子无法解释的 “内禀固有属性”&#xff0c;直接回避自旋的物理起源、微观结构与动力学成因&#xff0…

作者头像 李华
网站建设 2026/7/2 1:42:33

死磕信号量实现读者-写者:我被自己写的代码坑惨了

目录 一开始&#xff1a;我看到题&#xff0c;想先不看答案解决“经典问题” 第一回合&#xff1a;“完美”避开死锁&#xff0c;却撞上了死锁 第二回合&#xff1a;死锁修好了&#xff0c;又掉进了“并发度”的坑 第三回合&#xff1a;病急乱投医&#xff0c;想用“关中断…

作者头像 李华
网站建设 2026/7/2 1:42:06

出口工控硬件选型干货:工业 DC-DC/AC-DC 模块电源三点筛选标准丨国产化丨直流电源模块

一、引言&#xff1a;出口设备因模块电源选型失误引发批量出海故障反面案例当前国内工业自动化、测控仪器、智能装备厂商出海规模持续扩大&#xff0c;硬件工程师、电子工程师在整机研发阶段&#xff0c;常将设计重心放在主控电路、执行机构、通讯模块层面&#xff0c;轻视模块…

作者头像 李华
网站建设 2026/7/2 1:41:49

哈佛等联合研究团队揭开视频生成模型的致命盲区

这项由哈佛大学牵头&#xff0c;联合麻省理工学院、约翰斯霍普金斯大学、卡内基梅隆大学、波士顿大学、谷歌及MIT-IBM Watson AI实验室多机构完成的研究&#xff0c;于2026年6月25日以预印本形式发布&#xff0c;编号为arXiv:2606.27537。研究的核心成果是一个名为MemoBench的评…

作者头像 李华