嘿,各位编程爱好者!你有没有遇到过这样的场景:你的C++程序跑的好好的,突然哐当一下,给你弹出一个神秘的错误框,或者干脆就卡死不动了?这往往就是因为程序中发生了异常。
异常,顾名思义,就是程序在执行过程中遇到的不正常、非预期的事件。比如,你试图打开一个不存在的文件,或者对一个空指针进行解引用,再或者一个函数接收到了一个非法参数…….这些都可能导致异常。
如果不对这些异常进行处理,你的程序很可能就会像一个没有安全带的赛车手,一旦偏移赛道,就会直接撞墙。而C++提供了一套强大的机制来优雅地处理这些“意外情况”,这就是我们今天要深入学习的try-catch异常处理机制。
为什么需要try-catch?
在try-catch出现之前,我们通常会使用传统的错误处理方式,比如:
返回值检查:函数通过特定值(如 -1、false、nullptr)来表示错误。调用者需要每次都检查返回值
全局错误码:设置一个全局变量(如 errno),函数失败时设置它,调用者查询它。
这些方式固然能处理错误,但它们都有各自的缺点:
1.侵入性强:错误处理逻辑与正常业务逻辑混杂在一起,代码可读性变差。
2.容易遗漏:如果调用者忘记检查返回值或错误码,错误就会被忽略,导致程序继续执行,可能产生更严重的后果。
3.不能跨层级传播:如果一个函数在深层调用链中发生错误,你需要一层一层的返回错误码,非常繁琐。
4.资源泄露:在函数执行过程中如果发生错误并直接返回,可能导致之前申请的资源(内存、文件句柄等)没有被释放。
而 try-catch 机制,解决了这些问题
分离关注点:将正常的业务逻辑放在try块中,将错误处理逻辑放在catch块中,代码结构更清晰。
强制处理:当try块中发生异常时,程序会立即跳转到匹配的catch块,确保异常得到处理。
跨层级传播:异常可以从一个函数抛出,沿着调用栈一直向上传播,直到找到一个匹配的catch块,非常方便。
资源安全:配合析构函数和RAII(Resource Acquistion Is Initialization)机制,可以确保在异常发生时资源也能被正确释放。
try-catch 的基本语法
C++的try-catch语法非常直观:
try{
//可能会抛出异常的代码块
//如果这里发生了异常,控制流会立即跳转到匹配的catch块
}catch(ExceptionType1 ex1){
//捕获ExceptionType1类型的异常并处理
}catch(ExceptionType2 ex2){
//捕获Exceptiontype2 类型的异常并处理
}catch(...){ //捕获所有类型的异常(万能捕手)
//处理任何未被前面catch块捕获的异常
}
//try-catch 块结束后,程序继续执行
让我们逐一分解:
try块:
try{……}包含你认为可能抛出异常的代码
如果try块中的代码执行正常,catch 块会被跳过
如果try块中发生了异常(通常throw语句抛出),那么程序会立即中止try块中剩余的代码,并寻找匹配的catch块。
catch块:
catch(ExceptionType ex)用来捕获特定类型的异常。
ExceptionType是你期望捕获的异常类型(例如std::exception、std::runtime_error、自定义异常类型等)。你可以根据需要定义多个catch块来捕获不同类型的异常。
当try块中抛出的异常类型与某个catch 块的参数类型匹配时,该catch块的代码会被执行。
ex是一个异常对象,你可以通过它访问异常的详细信息(例如错误消息)。
catch(…)是一个特殊的catch块,它被称为一个特殊的catch块,它被成为通配符捕获或万能捕获。它可以捕获任何类型的异常。通常作为最后一个catch块,用于确保所有可能的异常都能被处理,但具体异常信息无法获取。
throw语句:
throw expression:用于抛出一个异常。
expression可以是任何类型的值,通常是一个异常类的对象。
当throw语句被执行时,当前函数的执行会被终止,控制流会沿着调用栈向上回溯,直到找到一个能够捕获该异常的try-catch块。
异常类型:标准库异常与自定义异常
C++中异常的类型非常灵活:
1.标准库异常
C++标准库提供了一系列预定义的异常类,它们都继承自std::exception。这使得你可以用统一的方式来处理各种标准错误。一些常用的标准异常包括:
std::exception:所有标准异常的基类。
std::bad_alloc:当new运算符失败(内存分配失败)时抛出
std::bad_cast:当dynamic_cast失败时抛出
std::logic_error:表示程序中出现的逻辑错误,例如:
std::domain_error:参数超出有效范围。
std::invalid_argument:函数接收到无效参数
std::length_error:试图创建一个长度超出限制的std::string或std::vector
std::out_of_range:访问容器时索引越界。
std::runtime_error:表示程序运行时发生的错误,例如;
std::overflow_error:算数上溢
std::underflow_error:算数下溢
std::range_error:计算结果超出表示范围
std::system_error:操作系统或底层API导致的错误
所有std::exception派生类都提供一个what()虚函数,返回一个C风格字符串,描述异常信息。
2.自定义异常
在实际项目中,标准库异常可能无法完全满足你的需求。你可以创建自己的异常类,通常是继承自std::exception(或其派生类),并重写what()函数,以便提供更具体的错误信息。
案例程序:从简单到复杂
让我们通过几个案例程序,一步步掌握try-catch的用法。
案例1:简单的文件操作异常
假设我们要读取一个文件,如果文件不存在,就抛出异常。
#include <iostream>
#include <fstream> // 文件流
#include <string>
#include <stdexcept> // 包含 std::runtime_error
// 尝试打开文件并读取第一行
std::string readFileFirstLine(const std::string& filename) {
std::ifstream file(filename);
// 检查文件是否成功打开
if (!file.is_open()) {
// 如果文件打不开,抛出一个 runtime_error 异常
throw std::runtime_error("Error: Could not open file '" + filename + "'");
}
std::string line;
if (std::getline(file, line)) {
return line;
} else {
// 如果文件是空的
throw std::runtime_error("Error: File '" + filename + "' is empty or reading failed.");
}
}
int main() {
std::string existingFile = "data.txt";
std::string nonExistingFile = "non_existent.txt";
// 1. 测试成功情况
// 为了测试成功情况,先创建一个 data.txt 文件
std::ofstream ofs(existingFile);
if (ofs.is_open()) {
ofs << "Hello, Exception Handling!" << std::endl;
ofs << "This is the second line." << std::endl;
ofs.close();
std::cout << "Created " << existingFile << " for testing." << std::endl;
} else {
std::cerr << "Error: Could not create " << existingFile << std::endl;
return 1;
}
std::cout << "\n--- Trying to read existing file ---" << std::endl;
try {
std::string firstLine = readFileFirstLine(existingFile);
std::cout << "Successfully read: \"" << firstLine << "\"" << std::endl;
} catch (const std::runtime_error& e) {
// 捕获 std::runtime_error 异常
std::cerr << "Caught runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 捕获其他 std::exception 及其派生类异常
std::cerr << "Caught general exception: " << e.what() << std::endl;
}
// 注意:这里没有 catch all,如果抛出非 std::exception 派生的异常,程序会终止。
std::cout << "\n--- Trying to read non-existent file ---" << std::endl;
try {
std::string firstLine = readFileFirstLine(nonExistingFile);
std::cout << "Successfully read: \"" << firstLine << "\"" << std::endl;
} catch (const std::runtime_error& e) {
// 捕获 std::runtime_error 异常
std::cerr << "Caught runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 捕获其他 std::exception 及其派生类异常
std::cerr << "Caught general exception: " << e.what() << std::endl;
}
std::cout << "\n--- End of program ---" << std::endl;
return 0;
}
运行输出:
Created data.txt for testing.
--- Trying to read existing file ---
Successfully read: "Hello, Exception Handling!"
--- Trying to read non-existent file ---
Caught runtime error: Error: Could not open file 'non_existent.txt'
--- End of program ---
代码解析:
1.readFileFirstLine函数中,我们传入文件名。
2.std::ifstream file(filename); 尝试打开文件。
3.if (!file.is_open()) 检查文件是否打开成功。如果失败,我们使用 throw std::runtime_error(…) 抛出一个运行时错误异常。std::runtime_error 构造函数接收一个字符串作为错误信息。
4.在 main 函数中,我们使用 try-catch 块来调用 readFileFirstLine。
5.当 readFileFirstLine 抛出 std::runtime_error 时,main 函数的 try 块会立即停止执行,跳转到 catch (const std::runtime_error& e) 块。
6.在 catch 块中,我们通过 e.what() 获取异常对象的错误描述字符串,并打印出来。
7.我们还添加了一个 catch (const std::exception& e),它会捕获所有继承自 std::exception 的异常,包括 std::runtime_error。注意 catch 块的顺序很重要,更具体的异常类型(如 runtime_error)应该放在更通用的异常类型(如 exception)之前,否则通用类型会先捕获到。
案例2:自定义异常类
自定义异常类可以让你更好的组织和区分不同类型的错误。
#include <iostream>
#include <string>
#include <stdexcept> // 包含 std::exception
// 1. 定义自定义异常类
class DivideByZeroException : public std::exception {
private:
std::string message;
public:
// 构造函数,接收一个错误消息
DivideByZeroException(const std::string& msg = "Division by zero is not allowed.")
: message(msg) {}
// 重写 what() 虚函数,返回异常描述
const char* what() const noexcept override {
return message.c_str();
}
};
// 2. 一个可能抛出异常的函数
double divide(double numerator, double denominator) {
if (denominator == 0) {
// 如果分母为0,抛出自定义异常
throw DivideByZeroException("Attempted to divide by zero in 'divide' function.");
}
return numerator / denominator;
}
int main() {
double num1 = 10.0;
double num2 = 2.0;
double num3 = 0.0;
std::cout << "--- Safe Division Attempt 1 ---" << std::endl;
try {
double result = divide(num1, num2);
std::cout << num1 << " / " << num2 << " = " << result << std::endl;
} catch (const DivideByZeroException& e) {
// 捕获自定义的 DivideByZeroException
std::cerr << "Caught Custom Exception: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 捕获其他任何 std::exception 及其派生类异常
std::cerr << "Caught General Exception: " << e.what() << std::endl;
}
std::cout << "\n--- Unsafe Division Attempt 2 ---" << std::endl;
try {
double result = divide(num1, num3); // 尝试除以零
std::cout << num1 << " / " << num3 << " = " << result << std::endl;
} catch (const DivideByZeroException& e) {
// 捕获自定义的 DivideByZeroException
std::cerr << "Caught Custom Exception: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 捕获其他任何 std::exception 及其派生类异常
std::cerr << "Caught General Exception: " << e.what() << std::endl;
}
std::cout << "\n--- End of program ---" << std::endl;
return 0;
}
运行输出:
--- Safe Division Attempt 1 ---
10 / 2 = 5
--- Unsafe Division Attempt 2 ---
Caught Custom Exception: Attempted to divide by zero in 'divide' function.
--- End of program ---
代码解析:
1.我们定义了一个 DivideByZeroException 类,它继承自 std::exception。
2.在构造函数中,它接收一个字符串作为错误消息,并将其存储在私有成员变量 message 中。
3.我们重写了 what() 虚函数,使其返回存储的错误消息。noexcept 表示这个函数不会抛出异常。
4.divide 函数现在会在分母为零时抛出 DivideByZeroException 对象。
5.在 main 函数中,我们首先尝试安全的除法,然后尝试除以零。
6.当 divide 函数抛出 DivideByZeroException 时,程序会跳转到 catch (const DivideByZeroException& e) 块,并打印出我们自定义的错误信息。
案例3:异常的传播与noexcept
异常可以跨函数调用栈传播。如果一个函数抛出异常,但它自己没有捕获,异常就会向上抛给调用它的函数,知道找到一个匹配的catch块。
noexcept关键字用于指示一个函数不抛出异常。如果一个声明为noexcept的函数抛出了异常,程序会立即终止(通过调用std::terminate()),而不是寻找catch块。这对于编译器优化和接口设计非常有用。
#include <iostream>
#include <string>
#include <stdexcept>
// 函数A:可能抛出异常
void functionA(int value) {
std::cout << "Entering functionA with value: " << value << std::endl;
if (value < 0) {
throw std::out_of_range("Value cannot be negative in functionA.");
}
std::cout << "Exiting functionA normally." << std::endl;
}
// 函数B:调用函数A,但不捕获异常
void functionB(int value) noexcept(false) { // noexcept(false) 显式声明可能抛出异常
std::cout << "Entering functionB." << std::endl;
functionA(value); // 函数A抛出的异常将在此处继续向上抛
std::cout << "Exiting functionB normally." << std::endl; // 这行代码可能不会执行
}
// 函数C:调用函数B,并在其内部捕获异常
void functionC(int value) {
std::cout << "Entering functionC." << std::endl;
try {
functionB(value);
std::cout << "functionB completed successfully inside functionC." << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Caught out_of_range in functionC: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught general exception in functionC: " << e.what() << std::endl;
}
std::cout << "Exiting functionC." << std::endl;
}
// 函数D:声明为 noexcept,因此它不应该抛出异常
// 如果它里面调用的函数A抛出异常,整个程序会终止。
void functionD(int value) noexcept {
std::cout << "Entering functionD (noexcept)." << std::endl;
// try-catch 可以在 noexcept 函数内部捕获其他函数抛出的异常
try {
functionA(value);
std::cout << "functionA completed successfully inside functionD." << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught (and handled) exception inside noexcept functionD: " << e.what() << std::endl;
// 注意:这里捕获并处理了异常,所以 functionD 本身没有 "抛出" 异常。
// 如果这里不捕获,而是让异常从 functionD 逃逸出去,程序就会终止。
}
std::cout << "Exiting functionD (noexcept)." << std::endl;
}
int main() {
std::cout << "--- Test Case 1: Exception Propagation ---" << std::endl;
functionC(-5); // functionA -> functionB -> functionC (捕获)
std::cout << "\n--- Test Case 2: No Exception ---" << std::endl;
functionC(10); // functionA -> functionB -> functionC (无异常)
std::cout << "\n--- Test Case 3: Noexcept Function Handling ---" << std::endl;
functionD(5); // functionA -> functionD (正常处理)
std::cout << "\n--- Test Case 4: Noexcept Function Violation (Will Terminate) ---" << std::endl;
// 故意创建一个 noexcept 函数会抛出异常的场景 (为了演示,通常应避免)
// 注意:这将导致程序异常终止,所以我们会将其注释掉,或者在测试时单独运行。
/*
try {
// 这里需要一个直接从 noexcept 函数内部抛出的异常才能准确演示
// 但为了简洁,这里演示的是 functionD 内部不捕获 A 抛出的异常
// 我们会修改 functionD 来演示
// functionD(-10); // 如果 functionD 内部不捕获,这将导致 terminate
} catch (...) {
// 外部捕获不会生效,因为 noexcept 抛出的异常会直接 terminate
std::cerr << "This catch will NOT be reached if functionD violates noexcept." << std::endl;
}
*/
std::cout << "\nProgram continues after exception handling." << std::endl;
return 0;
}
典型运行输出(不包含Noexcept违规导致terminate的部分):
--- Test Case 1: Exception Propagation ---
Entering functionC.
Entering functionB.
Entering functionA with value: -5
Caught out_of_range in functionC: Value cannot be negative in functionA.
Exiting functionC.
--- Test Case 2: No Exception ---
Entering functionC.
Entering functionB.
Entering functionA with value: 10
Exiting functionA normally.
Exiting functionB normally.
functionB completed successfully inside functionC.
Exiting functionC.
--- Test Case 3: Noexcept Function Handling ---
Entering functionD (noexcept).
Entering functionA with value: 5
Exiting functionA normally.
functionA completed successfully inside functionD.
Exiting functionD (noexcept).
--- Test Case 4: Noexcept Function Violation (Will Terminate) ---
Program continues after exception handling.
代码解析:
1.functionA: 可能会抛出 std::out_of_range 异常。
2.functionB: 调用 functionA。它自己没有 try-catch 块。如果 functionA 抛出异常,异常会向上传播到调用 functionB 的函数。noexcept(false) 是显式地告诉编译器该函数可能抛出异常(这是 C++11 的语法,C++17 默认函数就是 noexcept(false),所以通常可以省略)。
3.functionC: 调用 functionB,并用 try-catch 包裹。当 functionA 抛出的异常经过 functionB 传播到 functionC 时,functionC 的 catch 块会捕获并处理它。
4.functionD: 声明为 noexcept。这意味着 functionD 承诺不会抛出异常。如果在 noexcept 函数内部发生的异常没有被捕获并处理,导致异常从 noexcept 函数中“逃逸”出去,程序会立即调用 std::terminate() 并终止。 在这个例子中,functionD 内部使用了 try-catch 来捕获 functionA 抛出的异常,因此 functionD 本身并没有对外抛出异常,遵守了 noexcept 的承诺。
案例4:RAII与异常安全
RAII (Resource Acquisition Is Initialization),即“资源获取即初始化”,是C++中一种重要的编程范式,用于管理资源(如内存、文件句柄、互斥锁等)。其核心思想是:将资源的生命周期绑定到对象的生命周期。当对象被创建时获取资源,当对象被销毁时释放资源。
RAII与异常处理的结合,可以确保即使在发生异常时,资源也能被正确释放,从而实现异常安全。
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
// 模拟一个资源类:文件句柄
class FileGuard {
private:
std::ofstream file;
std::string filename;
public:
FileGuard(const std::string& fname) : filename(fname) {
file.open(fname);
if (!file.is_open()) {
// 构造函数中如果不能打开文件,抛出异常
throw std::runtime_error("Failed to open file: " + fname);
}
std::cout << "File '" << filename << "' opened." << std::endl;
}
// 析构函数:保证在对象销毁时关闭文件
~FileGuard() {
if (file.is_open()) {
file.close();
std::cout << "File '" << filename << "' closed." << std::endl;
}
}
// 模拟写入数据,可能抛出异常
void writeData(const std::string& data, bool induceError = false) {
if (induceError) {
throw std::runtime_error("Simulated write error during data processing.");
}
file << data << std::endl;
std::cout << "Data written: " << data << std::endl;
}
// 禁止拷贝构造和拷贝赋值,因为文件句柄通常不适合拷贝
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
};
// 模拟一个可能抛出异常的业务逻辑函数
void processAndWriteData(const std::string& fpath, const std::string& data, bool induceErrorInWrite = false) {
std::cout << "\n--- In processAndWriteData function ---" << std::endl;
// 创建 FileGuard 对象,自动管理文件资源
FileGuard fg(fpath); // 如果这里打开文件失败,会抛出异常,fg 不会成功构造
// 模拟一些计算或其他操作,可能抛出异常
if (data.empty()) {
throw std::invalid_argument("Data to write cannot be empty.");
}
fg.writeData(data, induceErrorInWrite); // 写入数据,可能模拟错误
std::cout << "--- Exiting processAndWriteData normally ---" << std::endl;
}
int main() {
std::string goodFile = "log.txt";
std::string nonExistDirFile = "non_existent_dir/log.txt"; // 模拟无法创建的路径
// 测试1: 正常处理,文件打开,数据写入,文件关闭
try {
std::cout << "=== Test Case 1: Successful operation ===" << std::endl;
processAndWriteData(goodFile, "Hello C++ Exception Safety!");
} catch (const std::exception& e) {
std::cerr << "Caught exception in main for Test Case 1: " << e.what() << std::endl;
}
std::cout << "Program continues after Test Case 1." << std::endl;
// 测试2: 写入数据时模拟异常,但文件句柄仍能安全关闭
try {
std::cout << "\n=== Test Case 2: Simulated write error ===" << std::endl;
processAndWriteData(goodFile, "This will cause an error on write.", true); // 模拟写入错误
} catch (const std::exception& e) {
std::cerr << "Caught exception in main for Test Case 2: " << e.what() << std::endl;
}
std::cout << "Program continues after Test Case 2." << std::endl;
// 测试3: 打开文件失败(构造函数抛出异常),文件句柄也不会泄露
try {
std::cout << "\n=== Test Case 3: Failed to open file (constructor error) ===" << std::endl;
processAndWriteData(nonExistDirFile, "Some data.");
} catch (const std::exception& e) {
std::cerr << "Caught exception in main for Test Case 3: " << e.what() << std::endl;
}
std::cout << "Program continues after Test Case 3." << std::endl;
std::cout << "\n--- Main program finished ---" << std::endl;
return 0;
}
运行输出:
=== Test Case 1: Successful operation ===
--- In processAndWriteData function ---
File 'log.txt' opened.
Data written: Hello C++ Exception Safety!
--- Exiting processAndWriteData normally ---
File 'log.txt' closed.
Program continues after Test Case 1.
=== Test Case 2: Simulated write error ===
--- In processAndWriteData function ---
File 'log.txt' opened.
Caught exception in main for Test Case 2: Simulated write error during data processing.
File 'log.txt' closed.
Program continues after Test Case 2.
=== Test Case 3: Failed to open file (constructor error) ===
--- In processAndWriteData function ---
Caught exception in main for Test Case 3: Failed to open file: non_existent_dir/log.txt
Program continues after Test Case 3.
--- Main program finished ---
代码解析:
1.FileGuard类:
它的构造函数负责打开文件。如果打开失败,它会抛出 std::runtime_error。
关键点: 它的析构函数 ~FileGuard() 负责关闭文件。
因为 FileGuard 对象是在 processAndWriteData 函数的栈上创建的,所以无论 processAndWriteData 是正常返回,还是因为抛出异常而提前退出,FileGuard 对象的析构函数都会被调用,从而保证文件句柄总是被关闭。这就是 RAII 的魔力!
2.processAndWriteData函数:
它创建了一个 FileGuard 对象 fg。
它模拟了两种可能抛出异常的情况:
data.empty():模拟业务逻辑中的参数校验错误,抛出 std::invalid_argument。
fg.writeData(…):模拟写入数据过程中可能发生的错误(即使文件已打开),抛出 std::runtime_error。
观察输出,即使在 writeData 内部抛出异常导致 processAndWriteData 提前退出时,FileGuard 的析构函数依然被执行,文件仍然被安全关闭了。这证明了 RAII 的异常安全性。
如果 FileGuard 构造函数本身就失败了(如文件路径非法),那么 fg 对象不会成功创建,也就不存在析构函数被调用来关闭文件的问题,因为根本就没有文件需要关闭。异常直接从构造函数传播出去。
std::current_exception和std::rethrow_exception
有时你可能想在catch块中捕获异常,做一些局部处理(比如记录日志),然后再次抛出该异常,让调用栈上更上层的catch 块也能处理它。这个可以使用std::current_exception()和std::rethrow_exception()。
std::current_exception():捕获当前正在处理的异常,返回一个std::exception_ptr。
std::rethrow_exception(ptr):重新抛出std::exception_ptr指向的异常。
#include <iostream>
#include <string>
#include <stdexcept>
#include <exception> // For std::exception_ptr, std::current_exception, std::rethrow_exception
void innerFunction() {
std::cout << " Inner function: About to throw." << std::endl;
throw std::runtime_error("Error from innerFunction!");
}
void middleFunction() {
std::cout << " Middle function: Entering try block." << std::endl;
std::exception_ptr eptr; // 用于存储异常指针
try {
innerFunction();
} catch (const std::exception& e) {
std::cerr << " Middle function: Caught exception: " << e.what() << std::endl;
std::cout << " Middle function: Logging the error..." << std::endl;
// 捕获当前异常的指针,以便稍后重新抛出
eptr = std::current_exception();
}
std::cout << " Middle function: After catch block." << std::endl;
if (eptr) {
std::cout << " Middle function: Re-throwing the exception." << std::endl;
std::rethrow_exception(eptr); // 重新抛出之前捕获的异常
// 这会使得异常继续向上层传播
}
std::cout << " Middle function: Exiting (if not re-thrown)." << std::endl;
}
int main() {
std::cout << "Main function: Entering try block." << std::endl;
try {
middleFunction();
} catch (const std::exception& e) {
std::cerr << "Main function: FINAL CATCH - Caught exception: " << e.what() << std::endl;
}
std::cout << "Main function: Program finished." << std::endl;
return 0;
}
运行时输出:
Main function: Entering try block.
Middle function: Entering try block.
Inner function: About to throw.
Middle function: Caught exception: Error from innerFunction!
Middle function: Logging the error...
Middle function: After catch block.
Middle function: Re-throwing the exception.
Main function: FINAL CATCH - Caught exception: Error from innerFunction!
Main function: Program finished.
代码解析:
1.innerFunction抛出一个std::runtime_error。
2.middleFunction 中的 try-catch 捕获了这个异常。它打印一条日志消息,然后使用 std::current_exception() 获取当前异常的指针并存储在 eptr 中。
3.在 middleFunction 的 catch 块之后,它检查 eptr 是否有效。如果有效,就调用 std::rethrow_exception(eptr) 重新抛出之前捕获的异常。
4.这个重新抛出的异常会继续向上传播,直到 main 函数中的 catch 块捕获到它。
这在需要分层处理异常的场景非常有用,例如:底层模块捕获异常并转换为更上层模块能理解的特定异常类型,或者在捕获后进行日志记录并重新抛出,让更高级的错误处理机制介入。
什么时候使用异常?什么时候不用?
异常处理机制很强大,但并非万能。它也有其适用的场景,也有不建议使用的场景。
建议使用异常的场景:
真正的非预期错误:例如内存分配失败、文件打开失败、网络连接中断、数据库操作失败。这些情况通常代表程序无法正常继续执行下去。
错误跨越多个函数层级:当错误发生在深层调用链中,并且你希望这种错误能够直接传递到上层,而不是通过层层判断返回值来传递时,异常非常高效。
构造函数错误:构造函数没有返回值,因此当构造失败时,抛出异常时唯一的报错方式。
资源管理(RAII):配合RAII机制,确保资源在异常发生时也能被正确释放。
不建议使用异常的场景:
可预期的、频繁发生的普通错误:例如,用户输入格式错误、查找数组中不存在的元素。这些情况应该通过返回值、状态码或std::optional/std::variant等方式来处理,而不是抛出异常,异常的开销较高,会影响性能。
控制流:不要使用异常来替代正常的程序控制流(如If-else、循环)。异常机制的设计目的时处理错误,而非条件跳转。
性能敏感代码:异常的抛出和捕获涉及堆栈展开,这会带来显著的性能开销。在性能要求极高的代码路劲中,应尽量避免使用异常。
接口设计不当:一个函数如果经常性地抛出异常,可能表明其设计有缺陷,或者其功能边界不清晰。
总结
try-catch
是 C++ 处理异常的核心机制,它使得程序在面对非预期错误时能够更加健壮和优雅。
- 使用
try
块包含可能抛出异常的代码。 - 使用一个或多个
catch
块来捕获和处理特定类型的异常。 - 使用
throw
语句抛出异常,可以是标准库提供的异常,也可以是自定义的异常。 - 利用
std::exception
及其派生类的多态性来统一处理各种异常。 - 结合 RAII 机制,确保在异常发生时资源也能被正确释放,实现异常安全。
- 理解异常的传播机制以及
noexcept
关键字的含义和作用。 - 明智地选择何时使用异常,避免滥用。
掌握了 try-catch
,你的 C++ 代码将变得更加稳定和可靠,能够更好地应对运行时的各种挑战!现在,是时候尝试将这些知识应用到你自己的项目中了!