尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
[C++ 核心规范] 为什么构造函数抛异常析构函数不执行?一文搞懂 RAII 哲学与三/五/零法则 | CppCon 精华
导读摘要你是否曾因漏写delete或fclose而导致内存泄露与文件句柄耗尽或者因多线程加锁后遗忘解锁而引发死锁灾难本文基于 CppCon 2022 Andre Kostur 的演讲“Back to Basics: RAII in C”用**“共享单车押金退还”与“全自动垃圾分类”**等通俗生活类比带你彻底搞懂 C 的灵魂设计模式 ——RAII资源获取即初始化。文章深入剖析了栈回溯Stack Unwinding的安全保障、构造函数抛异常时析构函数不运行的底层痛点、析构函数绝对不能抛异常的铁律并系统阐述了现代 C 的三/五/零法则与标准库智能指针实战。核心收获彻底掌握如何利用对象生命周期自动、安全地闭环管理任何系统资源规避悬垂资源隐患。 前言程序员最大的敌人 —— 遗忘在编写代码时我们经常需要操纵一些“昂贵且有限”的资源文件句柄、数据库连接、互斥锁、显存、物理内存等。每次使用这些资源时我们都必须遵循三部曲获取资源比如fopen成功使用资源释放资源比如fclose看似很简单对吧但现实中我们的代码往往充斥着各种return、break甚至是抛出异常throw。只要你遗忘了任何一个释放点资源就会像断线的风筝一样泄露出去。现代 C 给出的终极解决方案就是RAII (Resource Acquisition Is Initialization)—— 资源获取即初始化。 第一章什么是 RAII—— 从共享单车押金说起[!TIP]生活类比传统的资源管理像你去租一辆需要手动退押金的自行车。如果你骑完车急着去赶火车发生return或异常直接把车丢路边你的押金资源就永远退不回来了。RAII则像是免押金信用租赁。单车上装了智能定位锁只要你停好车人走开栈对象离开作用域系统感应到你已结束行程就会自动给你结算并上锁。在 C 中RAII 的核心思想是将资源的生命周期绑定到栈上对象的生命周期。// ❌ 传统手动释放容易发生泄露voidbad_function(){FILE*ffopen(config.json,r);if(!f)return;if(parse_failed()){// ⚠️ 这里 return 了f 泄露了return;}fclose(f);}// ✅ RAII 方式自动释放voidgood_function(){// std::ifstream 是标准库提供的 RAII 类std::ifstreamfile(config.json);if(!file.is_open())return;if(parse_failed()){return;// ✅ 即使在此处退出file 也会被自动安全关闭}}// file 离开作用域其析构函数被调用自动关闭文件句柄️ 第二章栈回溯与异常安全 —— 编译器给的“免死金牌”为什么当抛出异常时RAII 对象依然能被安全地释放这得益于 C 的栈回溯Stack Unwinding机制。当函数中发生异常throw时 ┌─────────────────────────────┐ │ 运行时的栈帧逐层向外退回 │ ├─────────────────────────────┤ │ 编译器沿途自动调用 │ -- 只要是构造完毕的对象 │ 每一个局部变量的析构函数 │ 其析构函数必然被执行 └─────────────────────────────┘voidrisky_action(){std::lock_guardstd::mutexlock(my_mutex);// 1. 成功加锁throwstd::runtime_error(Something went wrong!);// 2. 突然抛出异常}// 3. 栈回溯发生lock 对象的析构函数被强制调用锁被释放 第三章构造函数与析构函数的异常生死劫这是 Andre Kostur 在演讲中重点警告的两个底层核心逻辑3.1 致命痛点构造函数抛异常析构函数不执行[!IMPORTANT]C 核心规则如果一个对象的构造函数在执行中抛出异常编译器认为该对象“尚未构造成功”。既然对象不存在它的析构函数就绝对不会被运行这会导致一个极其严重的后果 ——如果一个类同时接管了多个裸资源且在后一个资源分配时抛出异常前面的资源将彻底泄露// ❌ 灾难示范在一个构造函数内管理两个裸指针classDoubleDanger{int*buffer1;int*buffer2;public:DoubleDanger(){buffer1newint[100];// 分配成功throwstd::runtime_error(OOM!);// 假设在这里抛出异常buffer2newint[200];// 还没执行}~DoubleDanger(){delete[]buffer1;// ❌ 由于构造函数抛出异常析构函数根本不运行delete[]buffer2;// buffer1 彻底内存泄露}};现代解决方案单一职责与 RAII 组合永远不要在一个类中直接操纵多个裸资源。应当使用已经包装好的智能指针成员// ✅ 安全典范使用标准库智能指针成员classDoubleSafe{std::unique_ptrint[]buffer1;std::unique_ptrint[]buffer2;public:DoubleSafe(){buffer1std::make_uniqueint[](100);// 成功构造// 即使下面这行报错抛出异常由于 buffer1 已经成功构造完毕// 编译器会负责自动析构 buffer1内存被完美释放buffer2std::make_uniqueint[](200);}// 无需手动写析构函数一切交给编译器};3.2 析构函数的铁律千万别让异常飞出来C 析构函数默认都是noexcept的。为什么当程序由于另一个异常正在进行栈回溯时如果某个对象的析构函数又抛出了一个新异常C 运行时会面临“不知道该处理哪一个”的绝境。此时程序会直接调用std::terminate()强制自杀。最佳实践析构函数内可能产生异常的所有操作如关闭数据库连接、刷新文件必须在析构函数内部try-catch消化掉绝对不能向外抛出 第四章零法则、三法则与五法则当你的类开始管理资源时必须处理好拷贝Copy与移动Move。4.1 三法则 (Rule of Three) —— 经典 C如果你手动编写了析构函数来释放资源那你很可能也需要手动编写拷贝构造函数和拷贝赋值运算符深拷贝否则默认的浅拷贝会导致**双重释放Double Free**崩溃。4.2 五法则 (Rule of Five) —— C11 性能飞跃在 C11 之后为了支持移动语义你还需要额外实现移动构造函数和移动赋值运算符。移后状态的处理在移动资源后必须将源对象内部的指针或句柄置空如nullptr或-1防止源对象被析构时把已被接管的资源给释放掉。classResourceWrapper{int*raw_ptr_nullptr;public:// 移动构造函数ResourceWrapper(ResourceWrapperother)noexcept:raw_ptr_(other.raw_ptr_){other.raw_ptr_nullptr;// 关键移后状态置空}~ResourceWrapper(){deleteraw_ptr_;// 如果 raw_ptr_ 是 nullptrdelete 是安全的}};4.3 零法则 (Rule of Zero) —— 现代 C 终极推荐[!TIP]零法则尽量避免手动编写任何析构、拷贝、移动函数。通过将裸资源封装在std::unique_ptr或std::vector等现成的 RAII 容器中让编译器自动为你生成所有正确的控制函数。 第五章标准库 RAII 工具箱实战5.1 内存独占std::unique_ptr// 自动拥有自动释放不能拷贝只能移动std::unique_ptrWidgetwstd::make_uniqueWidget();5.2 多线程加锁std::scoped_lock(C17)std::mutex mtx1;std::mutex mtx2;voidthread_safe_job(){// 同时锁住两个 mutex自动使用死锁避免算法std::scoped_locklock(mtx1,mtx2);// 执行任务...}// 自动解锁安全无忧 第六章专家深潜 —— 视频之外的重要资源管理技巧6.1 C20std::jthread自动合并的 RAII 线程包装器在 C11 中普通的std::thread并不是一个完美的 RAII 包装器。因为如果你直接销毁一个还在运行的std::thread且没有提前调用join()或detach()程序会直接崩溃。C20 引入了std::jthread它是完美的 RAII 线程对象自动加入在析构时自动调用join()等待线程结束。协作式中断支持通过std::stop_token优雅地请求线程停止。#includethread#includeiostreamvoidworker(std::stop_token st){while(!st.stop_requested()){std::this_thread::sleep_for(std::chrono::milliseconds(100));std::coutWorking...\n;}}intmain(){{std::jthreadjt(worker);// 启动线程std::this_thread::sleep_for(std::chrono::milliseconds(300));}// jt 离开作用域析构函数被调用自动发送 stop 信号并等待 join 完成std::coutThread finished safely.\n;}6.2 巧妙利用std::unique_ptr的自定义删除器Custom Deleter有些第三方 C 语言库分配资源后需要调用特定的函数如free_widget(Widget*)来释放。我们可以利用std::unique_ptr的自定义删除器不用自己写复杂的类就能包装它们// 第三方库 C 风格 APIstructCWidget{};CWidget*create_c_widget();voidfree_c_widget(CWidget*w);// 自定义删除器包装为 RAIIusingRAIIWidgetstd::unique_ptrCWidget,decltype(free_c_widget);voidrun(){RAIIWidgetwidget(create_c_widget(),free_c_widget);// 使用 widget.get() 传给其他 C 函数}// 离开作用域自动调用 free_c_widget零开销️ 第七章RAII 资源生命周期思维导图RAII 资源生命周期模型构造阶段获取资源并绑定对象生存阶段由栈对象持有销毁阶段离开作用域触发析构构造失败 (抛异常)析构函数不执行需要通过成员 RAII 链式释放已分配资源不能拷贝的独占性资源 (unique_ptr)转移所有权: 移动语义源对象置于空状态 (nullptr / -1)栈回溯 (Stack Unwinding)自动释放全部已成功构造的对象⚠️ 析构函数必须为 noexcept (禁抛异常) 总结记住两句话告别资源泄露构造拿资源析构放资源生命周期绑栈上这是 C 实现全自动、异常安全垃圾回收的银弹。拒绝裸资源组合拥抱零法则利用标准库智能指针把繁杂的深拷贝、移动语义操作直接留给编译器。“RAII is the most important feature that makes C C.”—— Bjarne Stroustrup (C 之父) 推荐阅读 系列文章SEO 长尾关键词布局C RAII 机制原理解析、构造函数抛异常析构函数不执行、C 栈回溯与异常安全、三五零法则 Rule of Five、std::unique_ptr 自定义删除器、C20 std::jthread 用法、防止多线程死锁 std::scoped_lock视频来源CppCon 2022 - Back to Basics: RAII in C - Andre Kostur本文作者立场以 C 标准专家视角结合视频内容与实际开发经验撰写。如有疑问欢迎评论区讨论
RELATED

相关推荐

[C++ 基础核心] 为什么 constexpr 指针编译报错?打破 declarations 结构与右左法则的语法高墙 | CppCon 精华

[C++ 基础核心] 为什么 constexpr 指针编译报错?打破 declarations 结构与右左法则的语法高墙 | CppCon 精华

导读摘要: 在 C 开发中,我们经常遇到难以读懂的复杂声明或令人困惑的编译报错。比如:为什么 constexpr int *p; 会直接报错,而 const int *p; 却能顺利通过?本博文基于 CppCon 2022 Ben Saks 的演讲 “Back to Basics:…

📅 2026/9/9 14:10:57
定时/计数器:从原理到实战,解锁单片机精准时序控制

定时/计数器:从原理到实战,解锁单片机精准时序控制

1. 定时/计数器:单片机的时间指挥官第一次接触51单片机时,你可能用过delay()函数做延时——让CPU空转循环来消耗时间。这种简单粗暴的方式就像让厨师亲自数秒煮鸡蛋,不仅效率低下,还会耽误其他工作。而定时/计数器就是为解决这个问…

📅 2026/8/20 20:33:55
远程桌面游戏手柄控制神器:RdpGamepad完全指南

远程桌面游戏手柄控制神器:RdpGamepad完全指南

远程桌面游戏手柄控制神器:RdpGamepad完全指南 【免费下载链接】RdpGamepad Remote Desktop Plugin for Xbox Gamepads 项目地址: https://gitcode.com/gh_mirrors/rd/RdpGamepad 还在为远程桌面连接时无法使用Xbox游戏手柄而烦恼吗?RdpGamepad正…

📅 2026/8/22 17:40:56
MORE NEWS

更多资讯

📰

SSM框架构建家电电商系统的核心技术解析

1. 项目概述:基于SSM的家用电器在线销售系统这个项目是一个典型的B2C电商平台,采用SSM(SpringSpringMVCMyBatis)框架技术栈实现。我在实际开发中发现,家电类电商系统相比普通商品销售平台有几个显著特点:商…

📰

llamafile 干净往返验证(Verify Clean):从 reset-repo 到补丁重放、干净构建与测试的完整指南

llamafile 干净往返验证(Verify Clean):从 reset-repo 到补丁重放、干净构建与测试的完整指南 【免费下载链接】llamafile Distribute and run LLMs with a single file. 项目地址: https://gitcode.com/GitHub_Trending/ll/llamafile …

📰

CKEditor实现Word图片自动上传的技术方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

华为 MetaERP 资产模块的数据一致性,不是靠“人工对账补平”,而是靠“单一事实源 + 事务封闭 + 子分类账转换 + 期间锁 + 自动对账 + 分布式事务补偿”六层机制叠出来的。下面按“为什么

华为 MetaERP 资产模块的数据一致性,不是靠“人工对账补平”,而是靠“单一事实源 事务封闭 子分类账转换 期间锁 自动对账 分布式事务补偿”六层机制叠出来的。 下面按“为什么不会乱 → 怎么保证不乱 → 乱了怎么发现”来讲。 一、先定边界&#…

📰

国内IC封测公司设备选型:晶圆键合与真空回流炉工艺解析

面对车规级功率器件、射频模组和MEMS传感器的量产需求,国内IC封测公司在产线规划中普遍面临一个共性难题:如何在有限的产线预算内,平衡晶圆键合、真空回流焊接与甲酸气氛保护这几道核心工序的场景覆盖。设备选型不当,轻则影响焊料…

📰

Univer 打印三件事:页面设置、预览、导出 PDF 一次说清

Univer 打印三件事:页面设置、预览、导出 PDF 一次说清 【免费下载链接】univer Univer is a full-stack framework for creating and editing spreadsheets / word processor / presentation on both web and server. 项目地址: https://gitcode.com/GitHub_Tren…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬