异常处理:try、throw、catch;异常安全;标准异常类;noexcept说明符

异常处理这个话题,说实话,很多C++程序员对它又爱又恨。爱它是因为它能优雅地处理错误,恨它是因为用不好反而会让代码变得一团糟。我在项目中见过太多滥用异常的例子,也见过因为不用异常导致代码臃肿不堪的情况。今天我们就来好好聊聊这个。

一、异常处理的基本机制:try、throw、catch

异常处理的核心思想很简单:把错误检测和错误处理分离开。你想想看,传统的错误处理方式是什么?函数返回错误码,调用方检查返回值。这种方式的问题在于,错误处理代码会散落在业务逻辑的各个角落,代码可读性极差。

C++的异常机制用三个关键字搞定:

  • 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;
}

这段代码很简单,但背后有几个关键点:

  • throw抛出的可以是任何类型的对象,但建议继承自std::exception
  • catch按类型匹配,可以写多个catch块处理不同类型的异常
  • catch(...)可以捕获所有异常,但一般只用在最外层兜底
我的习惯:我一般会先写具体的异常类型,最后写catch(...)兜底。这样既能精确处理已知异常,又不会漏掉未知异常。

二、异常安全:写代码要有底线

异常安全这个概念,说白了就是:当异常发生时,你的程序不能处于一个「半死不活」的状态。我早期写代码时吃过这个亏——资源没释放、数据状态不一致,都是血泪教训。

异常安全有三个级别:

级别 含义 示例
基本保证 异常发生后,资源不泄漏,对象处于有效但可能不一致的状态 vector的push_back失败时,vector大小不变
强保证 操作要么完全成功,要么回滚到操作前的状态 数据库事务的提交/回滚
不抛异常 保证函数永远不会抛出异常 swap操作、析构函数

实现异常安全的关键技术:

  • RAII:资源获取即初始化,用对象管理资源,析构函数自动释放
  • copy-and-swap:先拷贝一份,修改拷贝,如果成功就交换,失败则原对象不变
  • scope guard:作用域守卫,在离开作用域时自动执行清理操作
// 使用RAII实现异常安全的文件操作
class FileGuard {
    FILE* file_;
public:
    FileGuard(const char* filename, const char* mode) 
        : file_(fopen(filename, mode)) {
        if (!file_) {
            throw std::runtime_error("文件打开失败");
        }
    }
    
    ~FileGuard() {
        if (file_) {
            fclose(file_);
        }
    }
    
    // 禁止拷贝
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
    
    // 提供移动语义
    FileGuard(FileGuard&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }
};
我曾经踩过的坑:在构造函数中抛出异常,但忘记释放已经分配的资源。后来我学乖了,所有资源都用RAII管理,构造函数里只做一件事——初始化成员变量。这样即使抛出异常,已经构造好的成员变量也会自动析构。

三、标准异常类:别自己造轮子

C++标准库提供了一套完整的异常类体系,都继承自std::exception。我个人建议,除非有特殊需求,否则直接用标准异常类就好。

常用的标准异常类:

异常类 头文件 使用场景
std::runtime_error <stdexcept> 运行时错误,如除零、越界
std::logic_error <stdexcept> 逻辑错误,如参数无效、状态错误
std::bad_alloc <new> 内存分配失败
std::bad_cast <typeinfo> dynamic_cast失败
std::out_of_range <stdexcept> 容器访问越界
std::invalid_argument <stdexcept> 无效参数

如果你需要自定义异常,继承std::exception并重写what()方法即可:

class MyException : public std::exception {
    std::string msg_;
public:
    explicit MyException(const char* msg) : msg_(msg) {}
    const char* what() const noexcept override {
        return msg_.c_str();
    }
};

四、noexcept说明符:承诺的力量

noexcept是C++11引入的关键字,用来告诉编译器:这个函数不会抛出异常。为什么需要这个?因为编译器可以根据这个信息做优化,比如移动构造函数如果声明为noexcept,标准库容器在重新分配内存时就会使用移动而不是拷贝,性能提升明显。

使用场景:

  • 析构函数:默认就是noexcept,不要破坏这个约定
  • 移动构造函数和移动赋值:强烈建议声明为noexcept
  • swap函数:通常应该是noexcept
  • 简单访问器:比如getter函数,不会抛出异常
class Buffer {
    char* data_;
    size_t size_;
public:
    // 移动构造函数声明为noexcept
    Buffer(Buffer&& other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    // 移动赋值也声明为noexcept
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
    
    // 析构函数默认noexcept
    ~Buffer() {
        delete[] data_;
    }
};
小技巧:如果你不确定某个函数是否应该声明为noexcept,可以先不声明。等代码稳定后,用静态分析工具检查,再逐步加上noexcept。记住,noexcept是一个承诺,一旦违反程序就会直接终止。

五、异常处理的最佳实践

说了这么多,总结几条我在项目中一直遵循的原则:

  1. 不要用异常控制流程:异常是给异常情况用的,不是给正常逻辑用的。循环、条件判断才是控制流程的正确方式。
  2. 按值抛出,按引用捕获:throw异常对象时按值,catch时用const引用,避免对象切片和额外的拷贝。
  3. 保持异常层次扁平:不要搞太复杂的异常继承体系,够用就好。
  4. 在合适的地方捕获异常:不要在每个函数里都try-catch,让异常传播到能正确处理它的地方。
  5. RAII是异常安全的基础:所有资源都用RAII管理,这是C++异常安全的基石。

最后,用一张图来总结本章的知识体系:

异常处理知识体系 基本机制 • try:包裹可能抛出异常的代码 • throw:抛出异常对象 • catch:捕获并处理异常 异常安全 • 基本保证:资源不泄漏 • 强保证:操作可回滚 • 不抛异常:swap、析构函数 标准异常类 • std::exception 基类 • runtime_error / logic_error • bad_alloc / bad_cast / out_of_range noexcept说明符 • 承诺函数不抛异常 • 移动构造/赋值建议声明 • 违反承诺程序直接终止 核心原则:RAII + 异常安全 = 健壮的C++代码

异常处理不是银弹,但用好了确实能让代码更优雅、更健壮。记住一点:异常是用来处理「意料之外」的情况的,不是用来替代「if-else」的。把握好这个度,你的代码质量会上一个台阶。

公众号:蓝海资料掘金营,微信deep3321