C++ 异常处理
异常处理是 C++ 提供的一种结构化机制 ,用于在程序运行期间检测并处理错误。常见的运行时错误包括:
- 除以零
- 访问非法内存
- 文件读写失败
C++ 异常处理的工作方式
当错误发生时,不再让程序直接崩溃,而是通过"抛出 → 捕获 → 处理"的流程进行优雅恢复:
-
抛出异常(throw)
出现错误或意外情况时,用
throw关键字"抛出"一个异常对象。 -
捕获异常(catch)
程序沿着调用栈向上查找类型匹配 的
catch块,一旦找到就拦截该异常。 -
处理异常
catch块内编写应对逻辑,使程序可以恢复运行 或优雅退出。
try-catch 块
C++ 自有的异常处理机制:
- 把可能出错 的代码放进
try块; - 把处理错误 的代码放进
catch块。
语法:
cpp
try {
// 可能抛出异常的代码
}
catch (异常类型 变量) {
// 异常处理代码
}
执行流程:
一旦 try 块内抛出异常,立即中断 后续语句,跳转 到类型匹配的 catch 块执行;
若无匹配 catch,异常继续沿调用链向上传递。
抛出异常(Throwing Exceptions)
"抛出异常"就是从 try 块中返回一个代表错误的值 ,系统根据该值的类型寻找匹配的 catch 块。
使用关键字 throw 完成抛出。
语法:
cpp
try {
throw 表达式;
}
catch (类型 变量) {
// 处理代码
}
可抛出的值分三类:
- 内置类型
- 标准库异常类
- 自定义异常类
A.抛出内置类型
最简单,但几乎不带任何语义信息。示例:
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
int x = 7;
try {
if (x % 2 != 0){
throw -1; // 抛出 int 值
} // 抛出 int 值
}
catch (int e) { // 捕获 int
cout << "捕获异常: " << e;
}
return 0;
}
makefile
捕获异常: -1
缺点:必须靠错误码 判断问题,与 if-else 无异。C++ 推荐的做法是:抛出带有错误描述的类对象,而非裸值。
B.抛出标准异常
标准异常是一组已定义好的类 ,用于表示常见的各种错误。
这些类都位于头文件 <stdexcept> 中,并统一继承自基类 std::exception,形成一棵标准的异常类继承树。
(下图给出了 C++ 标准异常的层次结构。)

使用标准异常的好处:
- 语义清晰,可携带详细的错误信息;
- 与 C++ 标准库异常体系无缝衔接;
- 支持
what()成员函数,返回描述性字符串。 这些异常会被 C++ 标准库自身抛出,因此我们必须学会如何处理它们。
每个标准异常都提供what()方法,返回描述异常详情的字符串。
示例:
vector::at() 在索引越界时会抛出 out_of_range 异常。
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
try {
v.at(10); // 访问越界元素
}
catch (out_of_range e) {
cout << "捕获到: " << e.what();
}
return 0;
}
kotlin
捕获到: vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)
我们也可以用 throw 语句手动抛出这些标准异常。
| C++定义好的异常 | 主要用途 | 典型抛出场景 |
|---|---|---|
out_of_range |
访问超出有效范围 | vector::at()、string::at()索引越界 |
length_error |
试图创建超过最大长度的对象 | string构造函数长度过大,vector::reserve()超限 |
domain_error |
数学运算定义域错误 | 数学函数参数不在有效定义域内 |
invalid_argument |
函数参数无效 | 参数不符合函数要求 |
range_error |
计算结果超出有意义的值域 | 浮点数运算结果精度损失 |
overflow_error |
算术运算上溢 | 计算结果超出数据类型上限 |
underflow_error |
算术运算下溢 | 计算结果低于数据类型下限 |
C.抛出自定义异常
当标准异常无法满足需求时,可以自己写一个异常类 。
推荐继承 std::exception,这样就能与标准库的异常体系无缝衔接(当然不强制)。
完整示例:
cpp
#include <iostream>
#include <exception>
using namespace std;
/* ---------- 自定义异常类 ---------- */
class NegativeValueException : public exception {
private:
int value; // 保存出错数值
public:
explicit NegativeValueException(int val) : value(val) {}
// 重写 what(),返回错误描述
const char* what() const noexcept override {
return "出现负值错误!";
}
// 可选:提供接口获取当时传入的非法值
int getValue() const { return value; }
};
/* ---------- 可能抛出异常的函数 ---------- */
void checkValue(int x) {
if (x < 0)
throw NegativeValueException(x); // 抛出自定义异常对象
else
cout << "数值为: " << x << endl;
}
/* ---------- 使用示例 ---------- */
int main() {
int numbers[] = {10, -5, 20};
for (int n : numbers) {
try {
checkValue(n);
}
catch (NegativeValueException& e) { // 捕获自定义异常
cout << "捕获异常: " << e.what() << " 非法值 = " << e.getValue() << endl;
}
}
return 0;
}
makefile
数值为: 10
捕获异常: 出现负值错误! 非法值 = -5
数值为: 20
捕获异常catch
如前所述,catch 块用来接收并处理 try 块中抛出的异常。 catch 括号里必须写一个与抛出异常同类型的形参:
cpp
catch (异常类型 e) {
// 处理语句
}
其中 e 是给异常对象起的名字。 只有当 try 块里抛出的异常类型与这里的 异常类型 匹配时,才会进入该 catch 块执行其内部的代码。
A.捕获多种异常
一个 try 块后面可以跟多个 catch 块,用来分别处理不同类型的异常。例如:
cpp
try {
// 可能抛出多种异常的代码
}
catch (类型1 e) {
// 当抛出的异常是类型1 时执行
}
catch (类型2 e) {
// 当抛出的异常是类型2 时执行
}
catch (...) {
// 兜底:前面都没匹配到时执行
}
说明
- 编译器会按从上到下的顺序 寻找第一个类型匹配的
catch; catch(...)称为"通配捕获",可接住任何类型 的异常,常用来做最后兜底处理。
B.按值或按引用捕获
与函数传参一样,catch 也能"按值"或"按引用"接收异常对象,两者各有用途。
B1. 按值捕获
会复制一份被抛出的异常对象。 由于异常对象通常不大,复制开销一般可接受。
cpp
catch (runtime_error e) // 按值
{
cout << "Caught: " << e.what();
}
B2. 按引用捕获
直接绑定到原异常对象,无复制开销 ; 核心优势在于支持多态 :可以用基类引用捕获派生类异常,从而一个 catch 就能处理整个继承体系。
cpp
catch (exception& e) // 按引用(基类)
{
cout << "Caught: " << e.what();
}
示例中抛出的是 runtime_error,却被 exception& 成功捕获,这就是多态异常处理的典型用法。
异常传播
异常传播描述"异常在调用栈中如何向上传递"。
- 一旦
throw抛出异常,当前代码块立即终止 ,栈上已构造的局部对象会被自动析构 (栈回溯),但new出来的动态资源需要手动释放。 - 运行时沿着调用链逐帧向上 查找类型匹配的
catch块,这一过程称为 stack unwinding(栈展开)。 - 找到对应
catch即捕获并处理;若最终仍未匹配,程序调用std::terminate()终止。
示例流程:
cpp
#include <bits/stdc++.h>
using namespace std;
class GfG {
public:
GfG() { cout << "Object Created\n"; }
~GfG() { cout << "Object Destroyed\n"; }
};
int main() {
try {
cout << "Inside try block\n";
GfG gfg; // 构造局部对象
throw 10; // 抛出异常
cout << "After throw\n"; // 不会执行
}
catch (int e) {
cout << "Exception Caught\n";
}
cout << "After catch";
return 0;
}
javascript
Inside try block
Object Created
Object Destroyed ← 栈展开时自动析构
Exception Caught
After catch
嵌套 try-catch 块
在 C++ 里,try-catch 块可以嵌套 在另一个 try 或 catch 块内部。结构如下:
cpp
try { // 外层
// ... 可能抛 e2
try { // 内层
// ... 可能抛 e1
}
catch (eType1 e1) { // 内层只匹配 eType1
// 处理 e1
}
}
catch (eType2 e2) { // 外层可处理内层未接住的其他异常
// 处理 e2
}
- 若内层抛出的异常不是
eType1,内层catch不会执行,异常继续向外层传播 ,由外层catch尝试匹配。 - 如此可形成多级异常处理链。
重新抛出异常(Rethrow) 在内层 catch 中,可用空 throw 语句:
cpp
throw; // 必须位于 catch 块内
把已捕获的异常原样再次抛出 ,保留其类型与信息,供更外层 catch 继续处理。
这种方式常用于分层处理:底层记录日志/做清理,上层决定最终策略。
异常规格说明
C++ 允许在函数声明中显式说明该函数是否可能抛出异常。现代 C++ 只有两种规格:
| 规格 | 含义 |
|---|---|
noexcept |
保证 此函数不会抛出异常 ;若运行时仍抛出,将调 std::terminate() 终止程序。 |
noexcept(false) |
可能抛出异常 ;这是默认行为(不写任何规格时即为此)。 |
写法示例:
cpp
void func1(int a) noexcept // 承诺不抛异常
{
// ...
}
void func2(int b) noexcept(false) // 允许抛异常(可省略不写)
{
// ...
}
注:旧式
throw(type-list)规格已在 C++11 起被废弃 ,现代代码应使用noexcept。