24、异常处理:异常机制、try-catch-throw、标准异常类、自定义异常、异常安全

异常机制:为什么我们需要它?

写代码这么多年,我越来越觉得,异常处理就像汽车的保险带。平时你可能觉得它碍事,但真撞车的时候,它能救命。

你想想看,传统的错误处理方式是什么?函数返回一个错误码,调用方检查这个码。这有什么问题?

  • 容易忘:程序员一忙起来,很容易忘了检查返回值。然后程序带着错误往下跑,结果越跑越偏。
  • 代码臃肿:真正的业务逻辑被一堆 if-else 错误检查给淹没了。读代码就像在垃圾堆里找宝藏。
  • 构造函数没法返回错误码:构造函数没有返回值,你总不能让它返回一个 int 吧?

异常机制就是为了解决这些问题。它把「正常逻辑」和「错误处理」分开了。说白了,就是让代码更干净,更安全。

核心思想:异常发生的时候,程序会沿着调用栈向上跳,直到找到能处理这个异常的 catch 块。如果一直找不到,程序就崩溃。

try-catch-throw:三板斧

这三个关键字,是异常处理的基石。我习惯把它们叫做「三板斧」:

  • throw:抛出异常。就像你发现火情,拉响了警报。
  • try:监控一段可能出问题的代码。就像你在火警区域巡逻。
  • 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;
}

运行结果会直接打印出「捕获到异常: 除数不能为零!」。程序不会崩溃,而是优雅地处理了这个错误。

我的习惯:catch 的时候,尽量用引用捕获(比如 const std::exception&),这样可以避免对象切片,也能保留多态信息。

标准异常类:C++ 给你搭好的架子

C++ 标准库已经给我们准备了一套异常类,都定义在 <stdexcept> 头文件里。它们都继承自 std::exception

我整理了一张表,方便你查阅:

异常类 说明 常见场景
std::runtime_error 运行时错误 除零、文件打不开
std::logic_error 逻辑错误 参数越界、状态非法
std::invalid_argument 无效参数 传入了不合法的值
std::out_of_range 越界访问 vector 的 at() 方法
std::bad_alloc 内存分配失败 new 申请不到内存

我个人建议,能用标准异常就别自己造轮子。标准异常已经覆盖了 90% 的常见场景,而且大家都认识它,可读性更好。

自定义异常:当标准库不够用的时候

有一次我在项目中写一个网络库,需要传递「连接超时」和「DNS 解析失败」两种不同的错误。标准异常里没有这么细的类,怎么办?自己写一个。

自定义异常很简单,继承 std::exception,重写 what() 方法就行:

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

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

private:
    std::string message;
};

void connectToServer() {
    // 模拟连接失败
    throw NetworkException("连接超时,请检查网络");
}

int main() {
    try {
        connectToServer();
    } catch (const NetworkException& e) {
        std::cerr << "网络错误: " << e.what() << std::endl;
    }
    return 0;
}

注意:重写 what() 时,一定要加上 noexcept。因为 std::exception::what() 是 noexcept 的,如果你不加,编译器可能会报 warning,甚至在某些实现下行为未定义。

异常安全:写代码的底线

异常安全,说白了就是:当异常发生时,你的程序不会留下烂摊子。比如内存泄漏、文件句柄没关、数据状态不一致。

我把它分为三个等级:

  • 基本保证:异常发生后,程序处于有效状态,没有资源泄漏。但数据可能不是原来的样子。
  • 强保证:异常发生后,程序状态回滚到调用之前。就像什么都没发生过。
  • 不抛异常保证:这个操作绝对不会抛出异常。比如 int 的拷贝赋值。

嗯,这里要注意,强保证很难做到,但基本保证是底线。我曾经在一个项目中,因为没注意异常安全,导致数据库连接池泄漏,线上服务每跑 48 小时就得重启一次。后来排查了三天才找到原因——一个 new 操作抛了异常,导致连接没归还。

怎么做到异常安全?记住三个原则:

  1. RAII:资源获取即初始化。用智能指针管理内存,用 std::lock_guard 管理锁。这样异常发生时,析构函数会自动释放资源。
  2. 尽量少在构造函数里抛异常:构造函数抛异常,对象就没构造完成,析构函数不会被调用。如果你在构造函数里 new 了东西,得自己清理。
  3. 使用 copy-and-swap:先拷贝一份,修改拷贝,如果修改成功,再和原对象交换。这样即使修改过程抛异常,原对象纹丝不动。

一句话总结:异常安全不是锦上添花,而是雪中送炭。它决定了你的程序在出问题时,是优雅降级,还是直接爆炸。

知识体系总览

下面这张图,帮你把本章的知识点串起来。你可以把它当作一个思维导图来看:

异常处理 异常机制 分离正常逻辑与错误处理 栈展开(stack unwinding) try-catch-throw 抛出、监控、捕获 引用捕获避免切片 异常类体系 标准异常(std::exception) 自定义异常(继承 + what()) 异常安全(RAII / copy-and-swap) 基本保证 → 强保证 → 不抛异常保证

这张图把异常处理的四个核心模块串在了一起。你可以看到,异常安全是最终的落脚点——前面学的一切,都是为了写出更安全的代码。


好了,关于异常处理,我们就聊到这里。记住,异常不是洪水猛兽,用好了,它能让你的代码更健壮、更优雅。下次写代码的时候,不妨多想想:如果这里抛异常了,我的程序能扛得住吗?

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