【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是一个异常说明符,有两种使用形式:
noexcept:等价于noexcept(true),承诺函数绝对不抛出任何异常;noexcept(表达式):编译期判断表达式是否为true,决定是否承诺不抛异常(比如noexcept(std::is_nothrow_move_constructible_v<T>))。
它的核心价值:
- 性能优化:编译器知道函数不抛异常后,会省略栈展开、异常处理的额外代码;
- 行为控制:某些场景下(如容器移动),编译器会根据
noexcept决定是否采用更高效的逻辑; - 异常安全承诺:向调用者明确函数的异常行为,简化异常处理逻辑。
noexcept的核心使用场景:
- 移动构造 / 移动赋值运算符
这是noexcept最常用、最关键的场景 ——标准库容器(如std::vector、std::string)在扩容 / 移动时,只有当移动构造 / 赋值是noexcept时,才会选择 “移动” 而非 “拷贝”。
原因:容器需要保证 “异常安全”—— 如果移动过程中抛出异常,容器可能处于不一致状态(比如部分元素移动、部分未移动),而拷贝构造能保证失败时原数据不受影响。因此编译器默认策略是:
1.移动构造 / 赋值加noexcept → 容器用移动(高性能);
2.移动构造 / 赋值不加noexcept → 容器退化为拷贝(低性能,但异常安全)。
- 析构函数(默认
noexcept,建议显式声明)
显式声明noexcept的意义:
1.代码可读性更好,明确告诉开发者 “析构不抛异常”;
2.避免因基类 / 成员析构的异常属性导致析构函数变为noexcept(false)(比如手动声明~MyClass() noexcept {},强制保证不抛异常)。
- 纯静态 / 工具函数(无异常风险的函数)
对于那些逻辑简单、不可能抛出异常的函数(如数学计算、简单赋值、getter/setter),加noexcept能让编译器优化代码,同时明确异常行为。
绝对不要加noexcept的场景
noexcept是 “承诺”,如果函数实际抛出了异常,程序会直接调用std::terminate终止(无法捕获),因此以下场景绝对不能加:
- 可能抛出异常的函数:比如涉及文件 IO、网络请求、内存分配(
new可能抛std::bad_alloc)、越界检查的函数; - 拷贝构造 / 拷贝赋值:这类函数通常需要分配内存、复制数据,有抛异常风险(如
new失败),不能加noexcept; - 需要异常传递的函数:比如业务逻辑中的错误处理函数(需要通过异常向上层传递错误)
不抛保证(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函数、纯逻辑函数; - 原因:若它们抛异常,整个程序的异常安全会崩溃。