C++ 异常处理:try-catch 块与异常抛出
C++ 异常处理机制是管理程序运行时错误的强大工具,它允许将错误检测代码与错误处理代码分离,避免了传统错误码返回导致的深层嵌套 if-else 结构。通过异常处理,程序在遇到不可预见的错误时,能够自动跳转到合适的处理位置,同时自动清理沿途的栈资源。
1. 理解异常处理的基本三要素
C++ 异常处理围绕三个关键字展开:try、catch 和 throw。
- throw:当程序检测到错误时,抛出一个异常对象。这会中断当前函数的正常执行流程。
- try:定义一块代码区域,将其中的代码置于“监控”之下。该区域内的代码如果抛出异常,将被捕获。
- catch:紧跟在
try块之后,用于捕获并处理特定类型的异常。
2. 编写第一个 Try-Catch 块
最基础的异常处理结构包含一个 try 块和至少一个 catch 块。
- 打开你的 C++ 开发环境,创建一个新的源文件(如
main.cpp)。 - 输入以下代码,观察除数为零时的异常处理流程:
#include <iostream>
int main() {
int numerator = 10;
int denominator = 0;
try {
// 将可能出错的代码放入 try 块
if (denominator == 0) {
// 抛出一个整数类型的异常
throw -1;
}
std::cout << "Result: " << numerator / denominator << std::endl;
}
catch (int e) {
// 捕获整数类型的异常
std::cout << "Error: Cannot divide by zero. Error code: " << e << std::endl;
}
return 0;
}
- 编译并运行上述代码。由于
denominator为 0,程序不会执行除法操作,而是跳转到catch块输出错误信息。
3. 抛出标准异常对象
直接抛出整数或字符串虽然简单,但难以携带详细的错误信息。C++ 标准库提供了一系列异常类,定义在 <stdexcept> 头文件中,如 std::runtime_error、std::invalid_argument 等。
- 引入头文件
#include <stdexcept>。 - 修改代码逻辑,使用标准异常类替换原始的整数抛出:
#include <iostream>
#include <stdexcept>
double divide(int a, int b) {
if (b == 0) {
// 抛出标准异常对象,并附带错误描述信息
throw std::runtime_error("Division by zero is not allowed.");
}
return static_cast<double>(a) / b;
}
int main() {
try {
double result = divide(10, 0);
std::cout << "Result: " << result << std::endl;
}
catch (const std::runtime_error& e) {
// 使用 .what() 方法获取异常描述信息
std::cout << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
- 注意捕获语法:建议
const std::exception& e(引用传递),这样可以避免对象拷贝的开销,同时支持多态(基类引用捕获派生类对象)。
4. 掌握栈展开机制
当异常被抛出但未在当前函数内被捕获时,函数调用栈开始“展开”。这是异常处理的核心机制。
-
理解执行流程:
- 当前函数停止执行。
- 局部对象(在栈上创建的对象)的析构函数被自动调用。
- 控制权返回到上一层的调用者。
- 重复此过程,直到找到一个匹配的
catch块。如果直到main函数仍未捕获,程序将调用std::terminate终止。
-
查看下面的流程图,它展示了函数调用链中异常发生后的传递路径:
graph TD
subgraph Main ["Main 函数"]
M_Try["Try 块: 调用 FuncA"] -->|"无异常"| M_Success["正常结束"]
end
subgraph FuncA ["FuncA 函数"]
A_Try["Try 块: 调用 FuncB"] --> A_Catch["Catch 块: 捕获特定异常"]
end
subgraph FuncB ["FuncB 函数"]
B_Check["检查参数"] -->|"发现错误"| B_Throw["Throw 抛出异常"]
end
M_Try -->|"调用"| A_Try
A_Try -->|"调用"| B_Check
B_Throw -->|"异常传递: 栈展开\n(自动析构 FuncB 局部对象)"| A_Catch
A_Catch -->|"处理完毕"| M_Success
style B_Throw fill:#f9f,stroke:#333,stroke-width:2px
style A_Catch fill:#bbf,stroke:#333,stroke-width:2px
- 记住:在栈展开过程中,如果某个局部对象的析构函数内部抛出了异常且未被捕获,程序将直接崩溃,这被称为“异常逃离析构函数”,是必须避免的。
5. 使用 Catch-All 捕获所有异常
在实际开发中,可能无法预料所有会抛出的异常类型,或者希望作为最后的防线来记录未知错误。
- 添加一个特殊的
catch(...)块,它必须位于其他catch块之后。 - 编写代码如下:
try {
// 可能抛出任何类型异常的代码
// throw "Unknown error";
}
catch (const std::exception& e) {
// 处理标准库异常
std::cout << "Standard exception: " << e.what() << std::endl;
}
catch (...) {
// 捕获所有其他类型的异常(如 int, char*, 自定义类等)
std::cout << "Unknown exception caught. Performing cleanup..." << std::endl;
// 执行必要的清理工作(如释放内存、关闭文件)
// 注意:这里无法获取异常对象的具体信息
}
6. 异常规范与 Noexcept
C++11 引入了 noexcept 说明符,用于明确告知编译器和调用者该函数不会抛出异常。
- 标记函数为
noexcept。如果该函数内部抛出了异常(且未被内部捕获),程序将立即调用std::terminate,而不会尝试栈展开寻找匹配的catch。
void safeFunction() noexcept {
std::cout << "This function promises not to throw exceptions." << std::endl;
// 如果这里抛出异常,程序直接终止
// throw 1;
}
- 区分
noexcept(true)和noexcept(false)。默认情况下,普通函数等同于noexcept(false)。
7. 常用标准异常类速查
C++ 标准库提供了多种异常类以适应不同场景,下表列出了最常用的几种及其适用场景:
| 异常类名 | 头文件 | 适用场景 |
|---|---|---|
std::exception |
<exception> |
所有标准异常的基类,通常用于捕获通用异常 |
std::runtime_error |
<stdexcept> |
仅在运行时才能检测到的错误(如除零、内存不足) |
std::logic_error |
<stdexcept> |
程序逻辑错误(理论上可以通过代码检查避免,如无效参数) |
std::invalid_argument |
<stdexcept> |
向函数传递了无效参数(如传入负数给开根号函数) |
std::out_of_range |
<stdexcept> |
超出了有效范围(如访问数组或字符串越界) |
std::bad_alloc |
<new> |
new 操作符内存分配失败时抛出 |
8. 实际应用中的最佳实践
为了编写健壮的 C++ 代码,请遵循以下操作指南:
- 使用异常处理“真正异常”的情况(如文件不存在、网络断开),不要用于控制正常的程序流程(如循环结束判断)。
- 抛出异常时,优先抛出对象而非指针。抛出指针可能导致内存管理混乱(不知道是否应该
delete)。 - 捕获异常时,使用
const 引用(const T&)进行捕获。这样可以避免对象拷贝,支持多态,并防止修改异常对象。 - 确保析构函数绝对不抛出异常。如果在析构函数中需要进行可能失败的操作(如关闭文件),必须在内部使用
try-catch吞掉错误,而不是抛出去。 - 遵循 RAII(资源获取即初始化)原则。将资源封装在对象中,利用栈展开时析构函数自动调用的特性来管理资源释放,从而减少在
catch块中手动编写清理代码的需要。

暂无评论,快来抢沙发吧!