C++ 异常处理

在 C++ 中,异常处理(Exception Handling)提供了一种在运行时捕获、传播和处理错误的机制,使得错误处理逻辑与正常业务逻辑分离,提高代码可读性和健壮性。其核心由三部分构成:trythrow 和 catch,并辅以一系列语言和库特性。下面分章详解。


1. 异常的抛出:throw

  • 使用 throw 语句抛出一个异常对象,类型可以是内建或自定义类型(但最好是继承自 std::exception)。
  • 推荐抛出按值:throw MyError("message");,让异常对象在抛出时拷贝(或移动)到异常机制内部。
void readFile(const std::string& path) {
    std::ifstream in(path);
    if (!in) {
        throw std::runtime_error("Cannot open file: " + path);
    }
    // ...
}

2. 异常的捕获:try / catch

  • 在可能抛出异常的代码块外,用 try 括起;随后跟一组 catch 分支,按声明顺序匹配异常类型。
  • 捕获可按类型匹配,也可通过引用捕获以避免切片(object slicing)。
try {
    readFile("data.txt");
} catch (const std::runtime_error& e) {
    std::cerr << "运行时错误: " << e.what() << "\n";
} catch (const std::exception& e) {
    std::cerr << "其他标准异常: " << e.what() << "\n";
} catch (...) {
    std::cerr << "未知异常\n";
}
  • catch(...):捕获所有异常,但无法获取异常信息,只建议在最外层做兜底处理。
  • 捕获顺序:子类异常应放在基类之前,否则会被基类分支先捕获。

3. 异常类型设计

  • 标准异常:C++ 标准库提供了一系列异常类型,均继承自 std::exception
    • std::runtime_errorstd::logic_error 及其子类
    • std::bad_allocstd::bad_caststd::out_of_range 等
  • 自定义异常:继承自 std::exception 或其子类,并重写 what() 提供错误信息。
class FileError : public std::runtime_error {
public:
    explicit FileError(const std::string& msg)
      : std::runtime_error("FileError: " + msg) {}
};

4. 异常传播与栈展开

  • 当异常抛出后,控制流退出当前函数,开始 栈展开(stack unwinding):
    1. 调用当前函数的所有局部对象析构函数(RAII)
    2. 返回到调用者,继续寻找合适的 catch
  • 如果到达 main() 也未被捕获,调用 std::terminate(),程序通常会异常终止。

5. 资源管理与 RAII

  • RAII(Resource Acquisition Is Initialization)模式:将资源(内存、文件句柄、锁等)封装在对象构造/析构中管理,保证在异常发生时资源自动释放。
  • 智能指针std::unique_ptr / std::shared_ptr)和标准容器都支持 RAII,几乎免除裸指针的资源泄漏风险。
void process() {
    std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("data.bin","rb"), &fclose);
    if (!fp) throw FileError("open failed");
    // 读写文件
} // 即使抛出异常,fclose(fp.get()) 会被自动调用

6. noexcept 规范

  • C++11 引入 noexcept 关键字,用于标注函数 不会抛出异常
  • 优点:
    • 编译器可进行更多优化(如移动而非拷贝)
    • 在 noexcept 函数中抛出异常会调用 std::terminate(),确保行为一致。
void f() noexcept {
    // 保证不抛
}

void g() noexcept(false) {
    // 与不标注等价,可抛
}
  • STL 容器和算法在必要移动操作上会优先选择 noexcept 移动构造函数,以免在容器扩容时抛出导致资源不一致。

7. 异常安全保障等级

在编写能抛出异常的函数时,常考虑以下三种异常安全级别:

安全级别保证示例
基本保证出错时不会泄漏资源,保持对象处于有效状态,可能是部分更新向 std::vector push_back 失败后,不会泄漏内存,但元素可能少一个
强烈保证要么成功,要么状态不变(事务性)std::swap 通常提供强烈保证
不抛保证绝不抛出异常标注为 noexcept 的函数

设计时应努力提供至少 基本保证,在关键操作或简单辅助函数上争取 强烈保证


8. 常见误区与注意事项

  1. 不要滥用异常做流程控制
    异常开销高且会破坏控制流,应仅用于真正的“异常”情况,而非普通分支逻辑。
  2. 避免在析构函数中抛出异常
    在栈展开过程中如果析构再抛异常,会导致 std::terminate()。若有可能,析构中捕获并处理所有异常。
  3. 捕获对象时宜按常量引用
    catch (const MyException& e) 避免切片,并且不改变原异常对象。
  4. 保证基类有虚析构
    对于需要通过基类指针删除的对象,基类应声明 virtual ~Base().

小结

  • 抛出throw 一个符合接口的异常对象;
  • 捕获:用 try/catch 对分支捕获不同类型的异常,并按需处理或转抛;
  • RAII:结合构造/析构自动管理资源,确保在异常下资源不泄漏;
  • noexcept:在不会抛出的函数上标注,提升性能和安全性;
  • 异常安全:设计时至少提供基本保证,关键操作争取强烈保证。

掌握并遵循上述原则,能让你的 C++ 异常处理既安全可靠,又不会让代码变得臃肿或难以维护。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注