搞懂C++异常处理:你的代码再也不怕“突然崩溃”!

嘿,各位编程爱好者!你有没有遇到过这样的场景:你的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++ 代码将变得更加稳定和可靠,能够更好地应对运行时的各种挑战!现在,是时候尝试将这些知识应用到你自己的项目中了! ​

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇