跳转至

C++.Exception


C++中的异常处理是一种错误处理机制,允许程序执行过程中发生错误时转移到专门处理错误的代码中。

当程序遇到内存分配错误,文件无法打开等异常时,可以继续运行抛出错误,而不是直接崩溃。


1.Grammar

异常处理的核心组件:

  • try块:可能抛出异常的代码。
  • throw语句:用于抛出相关异常。
  • catch块:用于捕获并处理相关异常。

For example:

int main() {
    try {
        int num;
        cin << num;
        if(num == 1) {
            throw 42;   // 抛出整数异常
        } else if(num == 2) {
            throw "String Exception!";  // 抛出字符串异常
        } else {
            throw runtime_error("Unknown Exception!");  // 抛出标准异常
        }
    }
    catch(const int error) {
        cout << "caught int exception: " << error << endl;
    }
    catch(const char* error) {
        cout << "caught string exception: " << error << endl;
    }
    catch(const exception& error) {
        cout << "caught standard exception: " << e.what() << endl;
    }
    return 0;
}

The result of running:

2
caught string exception: String Exception!

2.标准异常类

C++中的标准异常类提供统一的异常处理接口,并且可以通过继承类自定义异常类。

常用标准异常类(std::exception类的派生): - std::logic_error:逻辑错误。 - std::runtime_error:运行时错误。 - std::bad_alloc:内存分配失败。

以下是std::exception基类的大致实现:

namespace std {
    class exception {
        exception() noexcept {}
        virtual ~exception() noexcept {}
        virtual const char* what() const noexcept;
    };
}

int main() {
    try {
        throw std::exception();
    }
    catch(const std::exception& error) {
        std::cout << "Default Exception: " << error.what << std::endl;
    }
    return 0;
}

std::exception基类中的虚函数what()非纯虚函数,有相关的默认实现,返回一个实现定义的字符串"std::exception"。

The result of running:

Default Exception: std::exception

可以自定义std::exception的派生类,重写基类中的what()。

For example:

class my_exception : public std::exception {
    private:
        std::string message;
    public:
        my_exception(const std::string& msg) : message(msg) {}
        ~my_exception() {}
        const char* what() const noexcept override {    // 重写虚函数时,限定符必须保持一致。
            return message.c_str();
        }
};

int main() {
    try {
        throw my_exception("This is custom exception!");
    }
    catch(const std::exception& error) {
        std::cout << "my_exception: " << error.what() << std::endl;
    }
    return 0;
}

The running of result:

my_exception: This is custom exception!

3.栈回卷(Stack Unwinding)

在复合调用的函数中若抛出异常,会向上匹配相关的catch块,在此过程中堆栈回卷,析构相关的局部资源。

若未匹配到相关的catch块,则调用std::terminate直接终止程序的运行,不进行堆栈回卷和析构局部资源。

For example:

void fun_3() {
    Test test_3(3);
    throw std::runtime_error("runtime_error in fun_3!");
}

void fun_2() {
    Test test_2(2);
    try {
        fun_3();
    }
    catch(const my_exception& error) {
        std::cout << error.what() << std::endl;
    }
    throw my_exception("my_exception in fun_2");
}

void fun_1() {
    Test test_1(1);
    try {
        fun_2();
    }
    catch(const my_exception& error) {
        std::cout << error.what() << std::endl;
    }
    throw my_exception("my_exception in fun_1");
}

int main() {
    try {
        fun_1();
    }
    catch(const std::exception& error) {
        std::cout << error.what() << std::endl;
    }
}

The result of running:

creating: 1
creating: 2
creating: 3
releasing: 3
releasing: 2
releasing: 1
runtime_error in fun_3!

由此看出,堆栈回卷和函数调用存在本质区别,堆栈回卷过程中不会执行后续的代码,会直接跳转的相关匹配的catch块。


4.异常处理表(EH Table)

C++中的异常处理表记录了代码中的try块的覆盖范围以及catch块的类型信息,在函数抛出异常时能够匹配到相关的catch块,正确执行堆栈回卷,析构局部对象。

  • 具体步骤:
    1. 中断当前执行流,捕获异常对象以及其类型信息。
    2. 逆向遍历调用栈,逐层向上查找匹配的catch块。
    3. 查询异常处理表:检查当前函数的指令指针是否在某个try块的start_ip和end_ip范围内。若在范围内,依次匹配该try块关联的catch块类型:
      • 若类型匹配(如catch (T)匹配抛出的异常类型T或其派生类),触发堆栈回卷并跳转到该catch块。
      • 若不匹配,继续向上层栈帧查找。
    4. 若未找到匹配的catch块,调用std::terminate()终止程序。

当无匹配的catch块时,会直接调用std::terminate()终止程序,不会执行堆栈回卷。

For example:

#include <iostream>
#include <stdexcept>

void inner() {
    throw std::runtime_error("Error"); // 抛出异常
}

void outer() {
    try {
        inner();
    } catch (int e) { // ❌ 类型不匹配
        std::cout << "Caught int\n";
    }
}

int main() {
    try {
        outer();
    } catch (const std::exception& e) { // ✅ 类型匹配
        std::cout << "Caught exception\n";
    }
    return 0;
}

运行时行为:

  1. inner() 抛出 std::runtime_error 异常。
  2. 运行时从 inner() 的栈帧开始查找异常处理表,但 inner() 无 try 块。
  3. 向上到 outer() 的栈帧,发现其 try 块覆盖了 inner() 的调用位置。
  4. 检查 catch (int e),类型不匹配。
  5. 继续向上到 main() 的栈帧,发现其 try 块覆盖了 outer() 的调用位置。
  6. 检查 catch (const std::exception& e),类型匹配。
  7. 触发堆栈回卷,析构 outer() 和 inner() 的局部对象,执行 main() 的 catch 块。

  8. 性能开销:

空间开销:异常处理表会增加二进制文件的大小(尤其是包含大量 try/catch 的代码)。

时间开销:运行时遍历调用栈并查询异常处理表有一定开销,但现代编译器(如 GCC、Clang)对 此高度优化。

某些编译器(如 MSVC)默认使用 Zero-Cost Exception Handling 策略:

  • 无异常时零开销:正常执行路径不携带异常处理信息。
  • 异常时代价较高:抛出异常时需动态生成堆栈回卷信息。