C++ 异常处理全指南:从基础抛出到 noexcept 优化

C++ 的异常处理机制(Exception Handling)是编写健壮、容错代码的关键。它允许我们将错误处理逻辑与正常业务逻辑分离,避免层层嵌套的 if-else 判断。

本文将结合五个核心示例文件,系统梳理 C++ 异常的抛出与捕获栈展开自定义异常类异常规范以及现代 C++ 中的 noexcept 优化。


一、异常处理基础:try-catch-throw

异常处理的核心由三个关键字组成:

  • throw:抛出异常,中断当前流程。
  • try:包裹可能抛出异常的代码块。
  • catch:捕获并处理特定类型的异常。

1.1 基本用法

#include <iostream>
using namespace std;

void divide(int a, int b) {
    if (b == 0) {
        throw "Error: Division by zero!"; // 抛出字符串常量
    }
    cout << "Result: " << a / b << endl;
}

int main() {
    try {
        divide(10, 0); // 触发异常
        cout << "This line will not be executed." << endl;
    } catch (const char* msg) {
        cerr << "Caught exception: " << msg << endl;
    }
    
    cout << "Program continues..." << endl;
    return 0;
}

输出:

Caught exception: Error: Division by zero!
Program continues...

1.2 捕获多种类型

可以使用多个 catch 块来处理不同类型的异常,也可以使用 ... 捕获所有未知异常。

try {
    // 可能抛出 int, string, 或自定义对象
} catch (int e) {
    cerr << "Integer error: " << e << endl;
} catch (const string& e) {
    cerr << "String error: " << e << endl;
} catch (...) {
    cerr << "Unknown error occurred!" << endl;
}

二、栈展开(Stack Unwinding)与资源安全

当异常被抛出时,程序会沿着调用栈向上查找匹配的 catch 块。在这个过程中,已经构造完成的局部对象会自动析构。这就是“栈展开”。

2.1 自动清理资源

这是 C++ 异常安全的重要保障(RAII 原则)。即使发生异常,栈上的对象(如 std::vector, std::fstream, 智能指针)也会正确释放资源。

class Resource {
public:
    Resource() { cout << "Resource acquired" << endl; }
    ~Resource() { cout << "Resource released" << endl; } // 异常时也会调用
};

void riskyFunction() {
    Resource res; // 栈对象
    throw 1;      // 抛出异常
    // res 的析构函数在此处自动调用
}

int main() {
    try {
        riskyFunction();
    } catch (int) {
        cout << "Exception caught" << endl;
    }
    return 0;
}

输出证明析构发生:

Resource acquired
Resource released
Exception caught

2.2 注意:裸指针的风险

如果使用的是裸指针(new 出来的对象),栈展开不会自动 delete 它,会导致内存泄漏。

  • 解决方案:始终使用智能指针(std::unique_ptr, std::shared_ptr)或容器来管理动态内存。

三、自定义异常类

抛出内置类型(如 int, string)通常不够专业。最佳实践是定义继承自 std::exception 的自定义异常类。

3.1 定义标准异常类

#include <exception>
#include <string>
#include <iostream>

class MyException : public std::exception {
private:
    std::string message_;
public:
    explicit MyException(const std::string& msg) : message_(msg) {}

    // 必须重写 what() 方法,返回 const char*
    const char* what() const noexcept override {
        return message_.c_str();
    }
};

void validateAge(int age) {
    if (age < 0) {
        throw MyException("Age cannot be negative!");
    }
}

int main() {
    try {
        validateAge(-5);
    } catch (const std::exception& e) { // 捕获基类引用
        std::cerr << "Standard Exception: " << e.what() << std::endl;
    }
    return 0;
}

3.2 优点

  • 类型安全:可以区分不同业务逻辑的错误。
  • 携带信息:可以在异常对象中存储错误码、文件名、行号等上下文信息。
  • 多态捕获:可以通过捕获基类 std::exception& 来统一处理所有标准异常。

四、异常规范与 noexcept (C++11/17)

在 C++98/03 中,可以使用 throw(type) 声明函数可能抛出的异常类型(动态异常规范),但在 C++11 中已被弃用。现代 C++ 推荐使用 noexcept

4.1 为什么需要 noexcept?

  1. 性能优化:编译器知道某函数绝不会抛出异常后,可以省略生成异常处理的开销代码(如栈展开表),显著提升性能。
  2. 移动语义优化:标准库容器(如 std::vector)在扩容时,如果元素的移动构造函数标记为 noexcept,则会使用高效的移动操作;否则会退化为安全的拷贝操作(防止移动中途抛出异常导致数据丢失)。

4.2 语法用法

// 1. 声明函数绝不抛出异常
void safeFunc() noexcept {
    // 如果这里抛出了异常,程序会直接调用 std::terminate() 终止
}

// 2. 条件 noexcept (根据表达式结果决定)
template<typename T>
void moveWrapper(T& t) noexcept(noexcept(t.move())) {
    t.move();
}

// 3. 查询是否 noexcept
if (noexcept(safeFunc())) {
    cout << "This function is safe." << endl;
}

4.3 重要规则

  • 析构函数默认 noexcept:在 C++11 及以后,类的析构函数默认是 noexcept(true)。如果析构函数内部可能抛出异常,必须显式处理(try-catch 吞掉),否则程序会崩溃。
  • 不要滥用:只有确定函数绝对不会抛出异常(或所有内部异常都已处理)时才使用 noexcept。如果标记了 noexcept 却抛出了异常,后果比未标记更严重(直接终止程序)。

五、综合实战示例

以下是一个整合了自定义异常、栈展开和 noexcept 的完整场景:

#include <iostream>
#include <vector>
#include <stdexcept>
#include <memory>

// 1. 自定义异常
class DatabaseError : public std::runtime_error {
public:
    explicit DatabaseError(const std::string& msg) 
        : std::runtime_error(msg) {}
};

// 2. 资源类 (RAII)
class DBConnection {
public:
    DBConnection() { std::cout << "DB Connected\n"; }
    // 析构函数必须是 noexcept 的
    ~DBConnection() noexcept { std::cout << "DB Disconnected\n"; }
};

// 3. 业务逻辑
void queryData(int id) {
    DBConnection conn; // 栈对象,异常时自动断开连接
    
    if (id < 0) {
        throw DatabaseError("Invalid ID");
    }
    
    std::cout << "Querying ID: " << id << "\n";
}

// 4. 标记为 noexcept 的安全函数
void cleanup() noexcept {
    std::cout << "Cleaning up resources...\n";
}

int main() {
    try {
        queryData(-1); // 触发异常
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    cleanup();
    return 0;
}

运行结果:

DB Connected
Error: Invalid ID
DB Disconnected  <-- 即使报错,连接也自动关闭了
Cleaning up resources...

六、最佳实践总结

  1. 按值抛出,按引用捕获
    • throw MyException(...)
    • catch (const std::exception& e)
    • 避免切片问题(Slicing)和多余的拷贝。
  2. 优先使用标准异常
    • 逻辑错误用 std::logic_error 及其子类(如 std::invalid_argument)。
    • 运行时错误用 std::runtime_error 及其子类。
  3. 慎用 noexcept
    • 用于移动构造函数、析构函数和确定不会失败的底层工具函数。
    • 不要在可能失败的业务逻辑函数上随意加 noexcept
  4. 异常不是控制流
    • 不要用异常来处理正常的逻辑分支(如“用户未找到”应返回 nullptroptional,而不是抛异常)。异常应当仅用于真正的错误情况
  5. 保证异常安全
    • 利用 RAII 管理资源,确保异常发生时没有资源泄漏。

通过合理使用 C++ 的异常机制,我们可以构建出既高效又具备强大容错能力的现代 C++ 应用程序。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐