C++异常处理机制
C++异常处理机制
-
抛出异常 (
throw
):-
当检测到错误或异常情况时,使用
throw
关键字抛出一个异常对象。 -
语法:
throw expression;
-
expression
可以是任意类型(基本类型、类类型、指针等),但强烈推荐使用派生自std::exception
的类对象(见下文)。 -
抛出异常后,当前函数的正常执行立即停止,控制权开始沿着调用栈向上寻找匹配的异常处理器 (
catch
块)。 -
示例:
double divide(double a, double b) {if (b == 0) {throw std::runtime_error("Division by zero!"); // 抛出一个标准异常对象}return a / b; }
-
-
尝试块 (
try
):-
定义一段可能抛出异常的代码区域。
-
语法:
try {// 可能抛出异常的代码statement1;statement2;// ... }
-
-
捕获块 (
catch
):-
紧跟在
try
块之后,用于捕获和处理在try
块内抛出的特定类型的异常。 -
可以有多个
catch
块,按顺序匹配异常类型。 -
语法:
catch (exception-declaration) {// 处理该类型异常的代码 }
-
exception-declaration
指定要捕获的异常类型。它可以是:- 具体类型 (
catch (std::runtime_error& e)
) - 引用(推荐,避免不必要的拷贝)(
catch (const std::exception& e)
) - 指针 (
catch (std::exception* e)
)(较少用) ...
(捕获所有异常,谨慎使用!)
- 具体类型 (
-
示例:
try {double result = divide(10.0, 0.0);std::cout << "Result: " << result << std::endl; } catch (const std::runtime_error& e) { // 捕获标准运行时错误std::cerr << "Runtime error occurred: " << e.what() << std::endl; } catch (const std::exception& e) { // 捕获任何标准异常std::cerr << "Standard exception occurred: " << e.what() << std::endl; } catch (...) { // 捕获所有其他类型的异常std::cerr << "Unknown exception occurred!" << std::endl;throw; // 重新抛出,让更上层的处理者处理 }
-
-
栈展开 (Stack Unwinding):
- 当异常被抛出后,程序沿着调用栈从当前函数向上回溯,寻找匹配的
catch
块。 - 在回溯过程中,栈上所有局部对象的析构函数会被自动调用(按照它们构造的相反顺序),这是异常安全性的关键机制(RAII)。
- 一旦找到匹配的
catch
块,栈展开停止,程序执行跳转到该catch
块。 - 如果回溯到
main
函数仍未找到匹配的catch
块,程序调用标准库函数std::terminate()
,通常导致程序异常终止。
- 当异常被抛出后,程序沿着调用栈从当前函数向上回溯,寻找匹配的
标准异常类 (<stdexcept>
)
C++ 标准库提供了一组预定义的异常类,都派生自 std::exception
。强烈建议使用它们或从它们派生自定义异常类,因为它们提供了统一的接口 (what()
成员函数) 来获取错误信息。
std::exception
: 所有标准库异常的基类。定义了virtual const char* what() const noexcept;
成员函数返回错误描述。- 逻辑错误 (Logic Errors): 通常表示程序内部的逻辑错误,应在编码阶段避免。
std::logic_error
std::domain_error
(参数值域错误)std::invalid_argument
(无效参数)std::length_error
(超出最大允许长度)std::out_of_range
(索引越界)
- 运行时错误 (Runtime Errors): 表示程序运行时发生的外部错误或无法在编码阶段完全预防的错误。
std::runtime_error
std::overflow_error
(算术上溢)std::underflow_error
(算术下溢)std::range_error
(结果超出有效范围)std::system_error
(封装操作系统错误码)
示例 (自定义异常):
class MyCustomError : public std::runtime_error {
public:MyCustomError(const std::string& message, int errorCode): std::runtime_error(message), code(errorCode) {}int getErrorCode() const { return code; }
private:int code;
};void riskyOperation() {if (somethingBadHappens) {throw MyCustomError("Specific error details", 42);}
}try {riskyOperation();
} catch (const MyCustomError& e) {std::cerr << "Custom Error (" << e.getErrorCode() << "): " << e.what() << std::endl;
}
关键特性与最佳实践
- RAII (Resource Acquisition Is Initialization):
- 异常安全性的基石。资源(内存、文件句柄、锁等)的获取应该在对象的构造函数中进行,资源的释放应该在析构函数中进行。
- 当异常导致栈展开时,局部对象的析构函数会被自动调用,从而确保它们持有的资源被正确释放,避免资源泄漏。
- 智能指针 (
std::unique_ptr
,std::shared_ptr
) 是 RAII 管理内存的完美例子。
- 异常安全保证 (Exception Safety Guarantees):
- 函数可以对其在抛出异常时的行为提供不同级别的保证:
- 基本保证 (Basic Guarantee): 抛出异常后,程序处于有效状态(无资源泄漏,对象处于可用但可能已修改的状态)。
- 强保证 (Strong Guarantee): 操作要么完全成功,要么失败并保持程序状态完全不变(像事务一样)。通常通过“复制-交换”惯用法或确保关键操作在修改状态前完成来实现。
- 不抛掷保证 (Nothrow Guarantee): 承诺该函数永远不会抛出任何异常。通常用
noexcept
关键字标记。析构函数通常应提供此保证。
- 函数可以对其在抛出异常时的行为提供不同级别的保证:
noexcept
说明符:- C++11 引入,用于声明函数是否承诺不抛出异常。
- 语法:
return_type function_name(parameters) noexcept;
或noexcept(expression)
(条件性)。 - 重要性:
- 性能优化: 编译器知道函数不抛异常后可以生成更高效的代码(无需准备栈展开信息)。
- 移动语义: 移动构造函数和移动赋值运算符通常应标记为
noexcept
,否则标准库容器(如std::vector
)在需要扩容时可能选择拷贝而非移动元素(为了强异常安全),导致性能下降。 - 契约: 向调用者明确表示该函数不会因错误而抛出异常。
- 析构函数、内存释放函数 (
operator delete
) 默认为noexcept
。
- 异常中立的函数:
- 函数本身不直接处理异常,但允许异常通过其自身传播给调用者。这是大多数普通函数的常态。它们需要确保在异常传播过程中自身资源得到清理(依赖 RAII)。
catch (...)
(捕获所有):- 捕获所有类型的异常。极其谨慎使用!
- 通常用于:
- 记录未知错误后重新抛出 (
throw;
)。 - 在程序终止前执行一些绝对必要的清理(但 RAII 通常是更好的选择)。
- 记录未知错误后重新抛出 (
- 问题:它捕获了所有异常(包括系统级异常、访问违例等),无法获取错误信息,破坏了类型安全。优先使用具体的异常类型或
std::exception&
。
异常 vs. 错误码
特性 | 异常 (Exceptions) | 错误码 (Error Codes) |
---|---|---|
传播方式 | 非局部 (自动沿调用栈向上) | 局部 (需要显式逐层返回/检查) |
可见性 | 强制处理 (未处理会导致程序终止) | 可选处理 (容易被忽略) |
错误类型 | 任意类型 (推荐派生自 std::exception ) | 有限类型 (通常整数或枚举) |
信息携带 | 丰富 (对象可包含详细错误信息、上下文) | 有限 (通常只是一个代码) |
流程控制 | 改变正常控制流 | 遵循正常函数返回流程 |
性能 | 正常路径快,异常路径慢 (栈展开开销大) | 路径恒定 (每次调用都需检查开销) |
适用场景 | 严重错误、构造函数失败、深层嵌套调用错误 | 预期内的错误、频繁发生的小错误、C接口 |
资源清理 | 自动 (通过栈展开和析构函数) | 手动 (在返回前清理) |
何时使用异常?
- 错误无法在本地有效处理: 需要报告给更高层的调用者。
- 构造函数失败: 构造函数没有返回值,异常是报告失败的主要机制。
- 操作严重失败且需要中止当前操作流: 如打开关键文件失败、内存分配失败、无效输入导致无法继续核心逻辑。
- 错误在调用栈深处发生: 使用错误码逐层返回非常繁琐且容易出错。
何时避免异常?
- 可预见的、频繁发生的、非关键性错误: 如用户输入无效、文件未找到(可能尝试其他路径)、网络请求超时(可能重试)。使用错误码或状态标志可能更高效、更清晰。
- 要求极高性能的代码路径: 异常处理机制有额外开销(即使未抛出)。
- 与 C 代码或 ABI 边界交互: C 语言不支持异常,跨越边界传播 C++ 异常会导致未定义行为。
- 实时系统或内存极度受限环境: 栈展开和异常处理的动态特性可能不可预测或资源消耗过大。
- 析构函数: 析构函数应避免抛出异常(通常标记为
noexcept
)。如果析构函数中可能失败的操作必须做,应内部处理掉异常(记录日志等),避免在栈展开过程中抛出新的异常(会导致std::terminate
)。
重要注意事项
- 不要将异常用于正常的控制流: 异常处理开销大,将其用于像循环终止这样的常规控制流是糟糕的设计和性能杀手。
- 按引用捕获 (
catch (const MyException& e)
): 避免对象切片(如果捕获基类)和不必要的拷贝开销。 - 异常规格 (Exception Specifications -
throw(type)
): C++98 的动态异常规格已被弃用 (C++11) 并移除 (C++17)。使用noexcept
代替。 - 异常安全: 设计类时,始终考虑在成员函数(特别是修改状态的函数)抛出异常时如何保证对象状态的有效性和避免资源泄漏。RAII 是核心。
- 重新抛出 (
throw;
): 在catch
块中使用throw;
(不带参数)可以重新抛出当前捕获的异常对象,允许更上层的处理器继续处理它。保留原始异常的类型和信息。
总结
C++ 异常机制是一种强大的错误处理工具,尤其适用于处理构造函数失败和需要非局部处理的严重错误。其核心在于 throw
、try
和 catch
关键字,以及栈展开过程中 RAII 保障的资源自动清理。理解标准异常类 (std::exception
及其派生类)、noexcept
说明符以及不同级别的异常安全保证对于编写健壮、可维护的 C++ 代码至关重要。然而,它并非万能,对于预期内的、频繁发生的错误或对性能要求极高的场景,错误码或状态检查可能是更合适的选择。明智地选择错误处理策略是良好 C++ 设计的关键部分。