Skip to content

C++ 异常处理

一个 异常(exception) 是指在程序执行期间发生的、中断正常指令流的意外事件或错误情况。例子包括尝试除以零、内存分配失败或遇到无效输入。

C++ 提供了一种结构化的机制,使用三个关键字来处理这些异常情况:try、catch 和 throw。

  • try:一个 try 块(try block) 包围了一段可能发生异常的代码。它表示您已准备好处理在此块内可能抛出的异常。
  • throw:当检测到错误情况时,使用 throw 关键字后跟一个表达式来 抛出(throw) 异常。表达式的类型决定了抛出的异常类型。
  • catch:一个 catch 块(catch block)紧跟在一个 try 块(或其他 catch 块)之后。它充当异常处理器。它指定可以捕获的异常类型,并包含处理该异常的代码。

基本语法如下所示:

try {
// 可能抛出异常的代码(受保护的代码)
// ...
} catch (const ExceptionType1& e1) { // 按常量引用捕获特定异常类型
// 处理 ExceptionType1
// 通过 e1 访问异常详情(例如,对于派生自 std::exception 的类型,使用 e1.what())
} catch (const ExceptionType2& e2) {
// 处理 ExceptionType2
// ...
} catch (...) { // 捕获所有类型的处理器(谨慎使用)
// 处理任何其他意外的异常类型
}

try 块内的代码正常执行。如果在 try 块内(或从 try 块调用的函数内)抛出异常,程序会立即跳转以寻找匹配的 catch 块。第一个类型与抛出的异常匹配(或为其基类)的 catch 块将被执行。如果找不到匹配的 catch 块,程序将终止(通常通过调用 std::terminate)。

您可以使用 throw 语句从任何地方抛出异常。操作数可以是任何类型,但强烈建议抛出派生自 std::exception 的类对象。

示例:抛出除以零的异常。

#include <stdexcept> // 标准异常类(如 std::runtime_error)所需
#include <string>
double safe_division(int a, int b) {
if (b == 0) {
// 抛出一个标准异常类型的对象
throw std::runtime_error("Division by zero condition!");
}
// 注意:整数除法会截断。为进行浮点除法而进行类型转换。
return static_cast<double>(a) / b;
}

catch 块指定了它可以处理的异常类型。最佳实践是按 常量引用 (const ExceptionType& e) 捕获异常。这可以避免不必要的拷贝,并在捕获多态异常类型时防止对象切片(slicing)。

try {
// ... 可能调用 safe_division 的代码 ...
double result = safe_division(10, 0);
// ... 如果抛出异常,这部分代码不会被执行 ...
} catch (const std::runtime_error& e) {
// 处理特定的运行时错误
std::cerr << "Error caught: " << e.what() << std::endl;
// e.what() 返回构造时传入的消息("Division by zero condition!")
}

您可以使用多个 catch 块来处理不同的异常类型。它们按顺序检查。一个捕获所有类型的处理器 catch (...) 会捕获任何类型的异常,但它不提供关于异常的任何信息。请谨慎使用它,通常用于在重新抛出之前进行清理。

演示 try/catch 的示例:

#include <iostream>
#include <stdexcept>
#include <string>
// 假设上面定义的 safe_division 函数在这里
double safe_division(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero condition!");
}
return static_cast<double>(a) / b;
}
int main() {
int x = 50;
int y = 0;
double z = 0;
try {
std::cout << "Attempting division..." << std::endl;
z = safe_division(x, y);
std::cout << "Result: " << z << std::endl; // This line won't execute
} catch (const std::runtime_error& e) {
// 按常量引用捕获特定的异常类型
std::cerr << "Caught a runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 捕获任何其他标准异常(如果 safe_division 抛出了不同的异常)
std::cerr << "Caught a standard exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception type." << std::endl;
}
std::cout << "Program continues after try-catch block." << std::endl;
return 0;
}

编译并运行后,将产生以下输出:

Attempting division...
Caught a runtime error: Division by zero condition!
Program continues after try-catch block.

C++ 标准库在 <stdexcept> 头文件(以及 <exception> 中的基类 std::exception)中定义了标准异常类层次结构。建议抛出这些标准类型(或从它们派生的类型)。

所有标准异常的基类是 std::exception。主要的派生类包括:

  • std::logic_error:表示程序内部逻辑错误(例如,前置条件被违反等)。通常在运行时之前理论上可以检测到。
  • std::domain_error:数学函数的域错误。
  • std::invalid_argument:传递给函数的无效参数。
  • std::length_error:尝试创建大于支持的最大尺寸的对象。
  • std::out_of_range:参数值不在预期范围内(例如,通过 .at() 访问超出范围的 vector 元素)。
  • std::runtime_error:表示可能只能在运行时检测到的错误。
  • std::overflow_error:发生算术溢出。
  • std::underflow_error:发生算术下溢。
  • std::range_error:计算结果无法表示。
  • std::system_error (C++11):表示低级别操作系统错误。
  • std::bad_alloc:当内存分配失败时由 new 抛出。
  • std::bad_cast:当引用转换失败时由 dynamic_cast 抛出。
  • std::bad_typeid:对空指针使用 typeid 时抛出。
  • std::bad_exception:用于非预期的异常处理机制。

所有派生自 std::exception 的类都有一个虚成员函数 what(),它返回一个 C 风格字符串(const char*)来描述异常。

您可以为特定于应用程序的错误定义自己的异常类。最佳实践是将自定义异常类派生自 std::exception 或其标准派生类之一(如 std::runtime_error 或 std::logic_error)。

#include <iostream>
#include <exception>
#include <string>
// 定义一个派生自 std::runtime_error 的自定义异常
class MyCustomException : public std::runtime_error {
public:
// 构造函数接受一个消息字符串
explicit MyCustomException(const std::string& message)
: std::runtime_error(message) {} // 将消息传递给基类构造函数
// 可选:如果需要,添加自定义成员或方法
// const char* what() const noexcept override; // 已由 std::runtime_error 提供
};
int main() {
try {
// 模拟错误情况
throw MyCustomException("Something specific went wrong in my application.");
} catch (const MyCustomException& e) {
std::cerr << "Caught MyCustomException: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 这将捕获可能发生的其他标准异常
std::cerr << "Caught standard exception: " << e.what() << std::endl;
}
return 0;
}

这将产生以下输出:

Caught MyCustomException: Something specific went wrong in my application.

编写异常安全(exception-safe)的代码至关重要。这意味着即使抛出异常,也要确保程序保持在有效状态。实现这一目标的主要 C++ 技术是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)。RAII 利用对象的生命周期(由构造函数和析构函数管理)来自动管理资源,例如内存、文件句柄、锁等。当抛出异常时,栈上对象的析构函数会自动调用,确保正确清理。智能指针(std::unique_ptr、std::shared_ptr)和标准库容器都大量依赖于 RAII。

您可以使用 noexcept 声明函数,指定它不会抛出任何异常。这可以带来编译器优化。如果一个 noexcept 函数确实抛出了异常,则会调用 std::terminate。析构函数通常应该是 noexcept 的。

void function_that_never_throws() noexcept { /* ... */ }