【C++】一篇文章了解C++的异常处理机制

【C++】一篇文章了解C++的异常处理机制

异常

基本异常处理关键字

在 C++ 中,异常处理是一种机制,用于处理程序在运行时发生的异常情况。异常是指程序执行期间发生
的意外事件,比如除以零、访问无效的内存地址等。通过使用异常处理机制,可以使程序更健壮,并能
够处理这些意外情况,避免程序崩溃或产生不可预测的结果。
在 C++ 中,异常处理通常包括以下关键词和概念:

  • try-catch 块: try 块用于标识可能会引发异常的代码块,而 catch 块用于捕获和处理异常。
    catch 块可以针对不同类型的异常进行处理。
  • throw 关键词: throw 用于在程序中显式抛出异常。当发生异常情况时,可以使用 throw 来抛出
    一个特定的异常类型。
  • 异常类型:异常可以是任何类型的数据,但通常是标准库中的异常类或自定义的异常类。标准库提
    供了一些常见的异常类,如 std::exception 及其派生类,用于表示不同类型的异常情况。

核心语法:

关键字作用关键注意点
throw中断当前代码流程,抛出异常对象,跳转到最近的匹配 catch抛出后,throw 之后的代码立即停止执行
try标记 “需要监控异常的代码块”,必须和至少一个 catch 配对仅监控 try 块内的代码,块外的异常不会被捕获
catch按 “类型从上到下” 匹配异常,处理捕获到的异常1. 建议用 const 引用(避免拷贝 + 防止切片);2. catch(...) 捕获所有异常

标准异常体系

C++ 内置了一套标准异常类(都继承自 std::exception),无需自定义就能满足大部分场景:

异常类用途头文件
invalid_argument无效参数(如除数为 0)<stdexcept>
out_of_range越界访问(如 vector::at()<stdexcept>
runtime_error通用运行时错误<stdexcept>
bad_alloc内存分配失败(new 失败)<new>
#include<iostream>#include<stdexcept>usingnamespace std;intdivide(int x,int y){if(y ==0)throwruntime_error("Division by zero error");elsereturn x / y;}intmain(){int x,y; cin >> x >> y;try{int result =divide(x,y); cout <<"Result: "<< result << endl;}catch(const runtime_error& e){ cout <<"Caught an exception: "<< e.what()<< endl;}return0;}

异常的传播

如果函数抛出异常但自身没有 catch,异常会 “沿着调用栈向上传播”,直到找到匹配的 catch

#include<iostream>#include<stdexcept>usingnamespace std;voidfunc3(){throwruntime_error("func3 抛异常");// 源头}voidfunc2(){func3();// 无 catch,异常继续传播}voidfunc1(){func2();// 无 catch,异常继续传播}intmain(){try{func1();}catch(const exception& e){ cout <<"main 捕获异常:"<< e.what()<< endl;// 最终在这里捕获}return0;}

异常安全

异常的核心问题是 “打断代码执行流程”,如果代码没做好防护,会导致两类严重问题:资源泄漏对象状态损坏

从上面的问题可以引出「异常安全」的核心定义:程序抛出异常后,依然保证 “资源不泄漏、对象状态有效(不损坏)、操作可预期” 的特性

C++ 把异常安全分为 4 个级别(从弱到强),重点掌握前 3 个:

安全级别核心定义对应解决的问题示例场景
1. 不抛保证(No-throw)函数绝不抛出任何异常(用 noexcept 标记),始终执行成功。避免函数抛异常引发的所有问题数学运算、析构函数、swap
2. 基本保证(Basic)异常抛出后,资源不泄漏、对象状态有效(但可能未完成预期操作)。解决资源泄漏 + 对象不损坏vector::push_back
3. 强保证(Strong)异常抛出后,对象状态回滚到异常前(操作要么全成,要么全败)。解决 “半修改” 的状态损坏问题业务核心数据修改
4. 不抛销毁(No-throw destruction)析构函数 / 资源释放函数绝不抛异常。避免栈展开时程序崩溃
noexcept关键字

noexcept是一个异常说明符,有两种使用形式:

  1. noexcept:等价于noexcept(true),承诺函数绝对不抛出任何异常;
  2. noexcept(表达式):编译期判断表达式是否为true,决定是否承诺不抛异常(比如noexcept(std::is_nothrow_move_constructible_v<T>))。

它的核心价值:

  • 性能优化:编译器知道函数不抛异常后,会省略栈展开、异常处理的额外代码;
  • 行为控制:某些场景下(如容器移动),编译器会根据noexcept决定是否采用更高效的逻辑;
  • 异常安全承诺:向调用者明确函数的异常行为,简化异常处理逻辑。

noexcept的核心使用场景

  • 移动构造 / 移动赋值运算符

这是noexcept最常用、最关键的场景 ——标准库容器(如std::vectorstd::string)在扩容 / 移动时,只有当移动构造 / 赋值是noexcept时,才会选择 “移动” 而非 “拷贝”

原因:容器需要保证 “异常安全”—— 如果移动过程中抛出异常,容器可能处于不一致状态(比如部分元素移动、部分未移动),而拷贝构造能保证失败时原数据不受影响。因此编译器默认策略是:

1.移动构造 / 赋值加noexcept → 容器用移动(高性能);

2.移动构造 / 赋值不加noexcept → 容器退化为拷贝(低性能,但异常安全)。

  • 析构函数(默认noexcept,建议显式声明)

显式声明noexcept的意义:

1.代码可读性更好,明确告诉开发者 “析构不抛异常”;

2.避免因基类 / 成员析构的异常属性导致析构函数变为noexcept(false)(比如手动声明~MyClass() noexcept {},强制保证不抛异常)。

  • 纯静态 / 工具函数(无异常风险的函数)

对于那些逻辑简单、不可能抛出异常的函数(如数学计算、简单赋值、getter/setter),加noexcept能让编译器优化代码,同时明确异常行为。

绝对不要加noexcept的场景

noexcept是 “承诺”,如果函数实际抛出了异常,程序会直接调用std::terminate终止(无法捕获),因此以下场景绝对不能加:

  1. 可能抛出异常的函数:比如涉及文件 IO、网络请求、内存分配(new可能抛std::bad_alloc)、越界检查的函数;
  2. 拷贝构造 / 拷贝赋值:这类函数通常需要分配内存、复制数据,有抛异常风险(如new失败),不能加noexcept
  3. 需要异常传递的函数:比如业务逻辑中的错误处理函数(需要通过异常向上层传递错误)
不抛保证(No-throw)
  • 语法:用 noexcept关键字标记函数,承诺 “绝不抛异常”;
// 标记 noexcept,编译器可优化,且调用者无需处理异常intadd(int a,int b)noexcept{return a + b;// 纯逻辑运算,不可能抛异常}// 析构函数默认 noexcept,必须保证不抛异常classSafeClass{private:int* p =newint(10);public:~SafeClass()noexcept{delete p;// delete 不会抛异常}};
基本保证(Basic)
  • 核心:用 RAII 保证资源不泄漏,允许对象 “未完成操作” 但状态有效;
#include<iostream>#include<stdexcept>#include<memory>usingnamespace std;voidtest(){ unique_ptr<int> ptr {make_unique<int>(42)};throwruntime_error("Intentional error for testing");}intmain(){try{test();}catch(const runtime_error& e){ cout <<"Caught an exception: "<< e.what()<< endl;}// 资源已释放,无泄漏;但函数未完成预期操作(符合基本保证)return0;}
强保证(Strong)
  • 核心:用 “Copy-and-Swap(拷贝并交换)” 实现 —— 要么全成,要么全败;
#include<iostream>#include<vector>#include<stdexcept>#include<algorithm>// swapusingnamespace std;classMyData{private: vector<int> nums ={1,2,3};public:// 强保证:修改前先拷贝,异常不影响原数据voidmodify(){// 步骤1:拷贝原数据到临时对象(副本) vector<int> temp = nums;// 步骤2:在副本上修改(抛异常也不影响原数据) temp[0]=100;throwruntime_error("测试异常"); temp[1]=200;// 步骤3:交换副本和原数据(swap 是 no-throw 操作)swap(nums, temp);}voidprint(){for(int num : nums) cout << num <<" "; cout << endl;}};intmain(){ MyData data;try{ data.modify();}catch(const exception& e){ cout << e.what()<< endl;}// 异常后,原数据完全没变化({1,2,3})→ 强保证 data.print();return0;}

异常安全关键手段有 3 个:

用 RAII 管理所有资源

  • 替代所有 “手动资源管理”:用 unique_ptr/shared_ptr 管理堆内存,用 fstream 管理文件,用 lock_guard 管理锁;
  • 核心逻辑:RAII 对象在栈上,离开作用域必析构,资源必释放 —— 无论是否抛异常。

遵循 “先拷贝,后修改”(强保证)

  • 修改对象前,先在 “临时副本” 上完成所有操作;
  • 只有副本操作全部成功(无异常),才用 swap(no-throw 操作)替换原对象;
  • swap 必须标记 noexcept,否则强保证会失效。

关键函数标记 noexcept

  • 必须标记 noexcept 的函数:析构函数、移动构造 / 赋值、swap 函数、纯逻辑函数;
  • 原因:若它们抛异常,整个程序的异常安全会崩溃。

Read more

Flutter 组件 analyzer_testing 适配鸿蒙 HarmonyOS 实战:分析器插件测试,构建 AST 仿真与编译器级别静态诊断验证架构

Flutter 组件 analyzer_testing 适配鸿蒙 HarmonyOS 实战:分析器插件测试,构建 AST 仿真与编译器级别静态诊断验证架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 analyzer_testing 适配鸿蒙 HarmonyOS 实战:分析器插件测试,构建 AST 仿真与编译器级别静态诊断验证架构 前言 在鸿蒙(OpenHarmony)生态迈向深度定制化研发、涉及高性能自定义 Lint 规则集开发、代码自动化重构工具链及严苛的编译器插件质量底线的背景下,如何实现一套能够精确模拟抽象语法树(AST)、支持在无文件系统环境下执行实时代码分析且具备“像素级”错误定位能力的“分析器测试基座”,已成为决定研发工具链稳定性与代码诊断准确性的命脉。在鸿蒙项目涉及海量 eTS 与 Flutter 代码混合静态检查的复杂场景下,如果开发的分析器插件未经严格的语法全集覆盖测试,由于由于分析引擎的内部状态复杂性,极易由于由于“误报”或“漏报”导致鸿蒙应用在编译期发生难以排查的元数据错误。 我们需要一种能够解耦物理磁盘、支持声明式代码片段输入且具备 AST 结构断言能力的验证方案。 analyzer_testing 为

By Ne0inhk
一文通关 MySQL 数据类型,打好高性能数据库的第一战!

一文通关 MySQL 数据类型,打好高性能数据库的第一战!

🔥海棠蚀omo:个人主页                 ❄️个人专栏:《初识数据结构》,《C++:从入门到实践》,《Linux:从零基础到实践》,《Linux网络:从不懂到不会》,《MySQL:新手入门指南》                 ✨追光的人,终会光芒万丈 博主简介: 目录 一.数值类型 1.1tinyint类型 1.2bit类型 二.小数类型 2.1float类型 2.2decimal类型 三.字符串类型 3.1char类型 3.2varchar类型 3.3char和varchar的比较 四.日期和时间类型 五.enum和set 5.1查询set中的数据 前言: 在上一篇文章中,我们学习了库和表的相关操作,而在我们上一篇的讲解中,我们提到了在列名后面跟的是数据类型,但是对于MySQL中的数据类型我们现在还一知半解,那么今天这篇文章我们就来详细谈一谈MySQL中的数据类型。 那么在详细讲解每种数据类型之前,

By Ne0inhk
【2025版】Spring Boot面试题

【2025版】Spring Boot面试题

文章目录 * 1. Spring, Spring MVC, SpringBoot是什么关系? * 2. 谈一谈对Spring IoC的理解 * 3. @Component 和 @Bean 的区别? * 4. @Autowired 和 @Resource 的区别? * 5. 注入Bean的方法有哪些? * 6. 为什么Spring 官方推荐构造函数注入? * 7. Bean 的作用域有哪些? * 8. Bean 是线程安全的吗? * 9. Bean 的生命周期了解吗? * 10. 如何解决 Spring 中的循环依赖问题? * 11. @Lazy能解决循环依赖问题吗? * 12. Spring 动态代理默认用哪一种? * 13. Spring中拦截器和过滤器的区别? * 14. Spring Boot的配置优先级 * 15. Spring Boot

By Ne0inhk