28、异常处理:异常机制(try/throw/catch)、标准异常类、noexcept说明符、异常安全保证

异常处理,说白了就是给程序装个「安全气囊」。

我刚开始写C++那会儿,总觉得异常处理是可有可无的东西。直到有一次,线上服务因为一个空指针直接崩溃,数据丢了整整两小时……嗯,从那以后,我再也不敢轻视异常了。

一、异常机制:try/throw/catch

C++的异常机制其实就三个关键字:trythrowcatch。它们配合起来,就像一套「报修-上报-处理」的流程。

  • try:圈定可能出问题的代码区域
  • throw:发现问题,抛出异常对象
  • catch:捕获并处理异常
#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::runtime_error("除数不能为零");
    }
    return a / b;
}

int main() {
    try {
        double result = divide(10.0, 0.0);
        std::cout << "结果: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "捕获异常: " << e.what() << std::endl;
    }
    return 0;
}

你想想看,如果没有异常机制,除零错误要么让程序崩溃,要么你得在每个调用处写一堆if判断。异常机制把错误处理从正常逻辑中剥离出来,代码干净多了。

我的习惯:我一般按引用捕获异常(catch (const std::exception& e)),避免对象切片,也省一次拷贝。

二、标准异常类

C++标准库提供了一套异常类体系,根类是std::exception。我个人觉得,用好这套体系,比你自己从头定义异常类要靠谱得多。

异常类 说明 常见场景
std::exception 所有标准异常的基类 捕获所有标准异常
std::logic_error 逻辑错误,可避免 参数校验失败
std::invalid_argument 无效参数 函数参数不合法
std::out_of_range 越界访问 vector::at() 越界
std::runtime_error 运行时错误,难避免 文件打开失败、网络超时
std::bad_alloc 内存分配失败 new 分配失败
我曾经踩过的坑:自定义异常类时,忘了继承 std::exception,结果 catch 基类时死活抓不到。后来我养成了习惯:所有自定义异常都继承 std::runtime_error,省心。

三、noexcept 说明符

noexcept 是C++11引入的关键字,用来告诉编译器:这个函数不会抛出异常。

void safe_func() noexcept {
    // 这里如果抛出异常,程序会直接终止
    std::cout << "这个函数保证不抛异常" << std::endl;
}

void maybe_throw() noexcept(false) {
    // 明确表示可能抛异常
    throw std::runtime_error("可能会抛");
}

为什么要有 noexcept?说白了,编译器知道函数不抛异常后,可以做更多优化。比如移动构造函数,如果标记为 noexcept,标准库容器在重新分配内存时会优先使用移动而非拷贝,性能提升明显。

避坑指南:我曾经把一个析构函数标记为 noexcept,结果里面调用了可能抛异常的函数。程序在析构时异常逃逸,直接调用了 std::terminate()。记住:析构函数默认就是 noexcept,千万别在里面抛异常。

四、异常安全保证

异常安全保证,说白了就是「出错了怎么办」。C++社区把它分成三个等级:

  • 基本保证:出异常后,程序状态仍然有效,但可能不是原来的值
  • 强保证:出异常后,程序状态回滚到操作之前
  • 不抛出保证:绝对不会抛异常
class Account {
    int balance_;
public:
    // 强保证:要么成功,要么状态不变
    void deposit(int amount) {
        if (amount < 0) throw std::invalid_argument("金额不能为负");
        int new_balance = balance_ + amount;  // 先计算
        if (new_balance < balance_) throw std::overflow_error("溢出");
        balance_ = new_balance;  // 最后才修改状态
    }

    // 不抛出保证
    int get_balance() const noexcept {
        return balance_;
    }
};

我建议你写代码时,至少保证「基本保证」。如果条件允许,尽量做到「强保证」。至于「不抛出保证」,留给那些绝对不能失败的操作,比如析构函数、swap函数。

我的经验:实现强保证的常用手法是「copy-and-swap」。先拷贝一份,在拷贝上操作,如果成功就swap,如果失败就丢弃拷贝。这样原始状态永远不会被破坏。

五、异常处理的核心流程图

C++ 异常处理核心流程 try 块开始 执行代码 有异常? 正常结束 catch 捕获处理 异常传播 throw 抛出 异常安全等级 基本 / 强 / 不抛出

六、总结

异常处理不是锦上添花,而是工程质量的底线。我见过太多项目因为异常处理不到位,线上出了事故才临时补丁。

记住三点:

  • 用标准异常类,别自己瞎造轮子
  • 能标记 noexcept 的地方尽量标记,编译器会感谢你
  • 写代码时想清楚异常安全等级,至少做到「基本保证」
最后说一句:异常处理就像买保险——你希望永远用不上,但真出事的时候,你会庆幸自己买了。

专注资料整理