C++ 标准异常

在实际编写程序时,我们并不需要每遇到一种错误都自己从头定义一个新的异常类。C++ 标准库已经提前准备好了一套现成的 “标准异常” 类,涵盖了日常开发中大多数常见的错误场景。

在这套标准异常体系中,“异常类型” 用来区分这是哪一类问题,而异常对象里保存的说明文字则用来告诉我们具体出了什么错。

C++ 标准异常体系

在 C++ 中,标准库的异常类通过继承构成了一个清晰的家族体系。最顶层的总基类(父类)是定义在 <exception> 头文件中的 std::exception,而日常开发用到的具体异常子类大多定义在 <stdexcept> 中。其中,stdexcept 是 “standard exception(标准异常)” 的缩写。

在 std::exception 基类之下,<stdexcept> 中的标准异常主要分为两大分支:一种是以 std::logic_error 为代表的 “逻辑错误”,说白了就是在调用函数前检查一下参数就能避免的错误;另一种是以 std::runtime_error 为代表的 “运行时错误”,也就是运行期间受外部环境影响才出现的错误。

此外,<new> 头文件中还定义了使用 new 申请动态内存失败时自动抛出的 std::bad_alloc 异常。常见的标准异常类如下表所示。

常用标准异常类
异常类型 所属头文件 说明
std::exception <exception> 所有标准异常的总基类,提供统一的 what() 函数
std::logic_error <stdexcept> 逻辑错误基类,表示调用前提或内部逻辑不成立
std::invalid_argument <stdexcept> 无效参数(logic_error 子类),表示实参值不符合要求
std::out_of_range <stdexcept> 越界错误(logic_error 子类),表示下标或位置超出范围
std::length_error <stdexcept> 长度错误(logic_error 子类),表示超出容器最大长度限制
std::runtime_error <stdexcept> 运行时错误基类,表示运行期间发生的一般性失败
std::bad_alloc <new> 内存分配失败,使用 new 申请内存失败时自动抛出

上表列举这些异常类只是方便小伙伴们平时查询,并不是叫大家死记硬背。对于初学的小伙伴来说,实际开发中最常用的是 std::runtime_error、std::invalid_argument 和 std::out_of_range 这 3 种,先把它们掌握好就足够应对大多数场景了。

作为所有标准异常的总基类,std::exception 提供了一个统一的成员函数 what()。在使用 try...catch 捕获异常时,只要我们用 const std::exception& e 接收对象,调用 e.what() 就能拿到抛出异常时填入的错误说明文字。

catch (const std::exception& e) {
    std::cout << e.what() << '\n';    // 输出错误说明文字
}

提示 平时写代码时,如果同时用到了 std::runtime_error 等具体子类和基类 std::exception,建议把 <stdexcept> 和 <exception> 这两个头文件都显式引入。这样可以避免更换编译器时因头文件包含关系不同而报错。

3 种常用的标准异常

定义在 <stdexcept> 中的具体标准异常类,创建和抛出的写法完全一致。

语法:

throw 异常类名(错误说明);

说明:

类名后面的小括号里用于传入一段描述错误原因的文字,既可以直接传入字符串字面量(如 "参数错误"),也可以传入 std::string 字符串变量。下面我们就来看看最常用的 3 种标准异常的具体用法。

1. std::runtime_error

std::runtime_error 是 “运行时错误” 的意思,适合用来表示程序在运行过程中才暴露出来、且当前函数无法继续往下执行的一般性失败。比如配置文件读取失败、网络连接中断或外部服务未就绪等场景,我们都可以抛出该异常。

示例 1:抛出 runtime_error

#include <iostream>
#include <stdexcept>
#include <string>

void loadConfig(const std::string& filename, bool exists) {
    if (!exists) {
        throw std::runtime_error("读取文件失败:" + filename);
    }
    std::cout << "配置文件加载成功\n";
}

int main() {
    try {
        loadConfig("app.conf", false);
    } catch (const std::runtime_error& e) {
        std::cout << "运行时错误:" << e.what() << '\n';
    }
    return 0;
}

运行结果如下。

运行时错误:读取文件失败:app.conf

分析:

在这个例子中,loadConfig("app.conf", false) 模拟了运行期间找不到配置文件的情况。因为形参 filename 是 std::string 类型,抛出异常时我们可以直接通过字符串拼接把具体文件名附带进去,方便在外层通过 e.what() 看清是哪个文件出了问题。

需要注意的是,小伙伴们不要把 std::runtime_error 当成偷懒的 “万能异常”。只有当错误确实属于运行期间的执行故障,且没有更贴切的参数或越界异常类可用时,我们才选择它。

2. std::invalid_argument

std::invalid_argument 中的 “invalid argument” 是 “无效参数” 的意思,专门用来表示调用者传入的参数值违反了函数的要求。比如设置年龄的函数规定年龄必须在 0 到 150 之间,一旦传入负数,我们就可以抛出 std::invalid_argument。

示例 2:抛出 invalid_argument

#include <iostream>
#include <stdexcept>

void setAge(int age) {
    if (age < 0 || age > 150) {
        throw std::invalid_argument("年龄必须在 0 到 150 之间");
    }
    std::cout << "年龄设置成功:" << age << '\n';
}

int main() {
    try {
        setAge(20);
        setAge(-5);
    } catch (const std::invalid_argument& e) {
        std::cout << "参数错误:" << e.what() << '\n';
    }
    return 0;
}

运行结果如下。

年龄设置成功:20
参数错误:年龄必须在 0 到 150 之间

分析:

先看第 1 次调用 setAge(20),传入的年龄落在合法范围内,函数正常打印成功提示。第 2 次调用 setAge(-5) 时触发了 age < 0 条件,函数直接抛出 std::invalid_argument 异常,明确告知调用者实参不符合要求。

3. std::out_of_range

std::out_of_range 中的 “out of range” 是 “超出范围” 的意思,专门用来表示下标或位置超出了允许的有效区间。比如容器里只有 3 个元素,有效下标是 0 到 2,如果尝试访问下标 3 或更大的位置,抛出 std::out_of_range 最贴切。

此外,标准异常并不只是在我们手动写 throw 时才会出现。C++ 标准库本身的许多组件,比如 vector 容器或 std::string 的 at() 成员函数,在检测到下标越界时也会自动抛出 std::out_of_range 异常。下面我们来看一个例子。

示例 3:at() 自动抛出越界异常

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> langs = {"C++", "Python", "Go"};

    try {
        std::cout << langs.at(3) << '\n';    // 访问下标 3 触发越界异常
    } catch (const std::out_of_range& e) {
        std::cout << "捕获到越界异常:" << e.what() << '\n';
    }
    return 0;
}

运行结果如下。

捕获到越界异常:vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)

分析:

langs 容器里一共有 3 个元素,有效下标是 0 到 2。执行 langs.at(3) 访问下标 3 时,at() 检测到越界,就会自动抛出 std::out_of_range 异常并交由下方的 catch 分支处理。

运行结果中打印的那串英文是标准库自带的越界提示,意思是访问的下标 3 大于或等于当前容器长度 3。这里顺带提醒一句:如果你使用方括号 langs[3] 越界访问,容器不会做边界检查,也不会抛出异常。只有调用 at() 成员函数,越界时才会自动抛出 std::out_of_range。

标准异常的选择与统一捕获

认识了上面 3 种常用的标准异常后,平时写代码遇到错误时我们应该选哪一种呢?可以按以下 3 条标准快速判断:

  • 参数值本身不合规则:比如年龄传了负数、除数不能为 0 却传了 0,优先选择 std::invalid_argument。
  • 下标或位置超出有效范围:比如只有 3 个元素却去读取下标 5,优先选择 std::out_of_range。
  • 运行中途任务无法继续:比如参数本身合法,但文件打不开或外部服务未就绪导致任务中断,选择 std::runtime_error。

下面我们通过一个完整的例子,看看如何在函数内部根据不同失败原因抛出具体的标准异常,并在外层使用总基类 std::exception 统一接住它们。

示例 4:抛出多种异常并统一捕获

#include <exception>
#include <iostream>
#include <stdexcept>

// 根据编号读取成绩
int getScore(int index, bool isReady) {
    if (index < 0) {
        throw std::invalid_argument("编号不能为负数");
    }
    const int scores[3] = {90, 85, 96};
    if (index >= 3) {
        throw std::out_of_range("编号超出有效范围");
    }
    if (!isReady) {
        throw std::runtime_error("成绩数据尚未就绪");
    }
    return scores[index];
}

void printScore(int index, bool isReady) {
    try {
        int score = getScore(index, isReady);
        std::cout << "读取成功,成绩:" << score << '\n';
    } catch (const std::exception& e) {
        std::cout << "读取失败:" << e.what() << '\n';
    }
}

int main() {
    printScore(-1, true);
    printScore(5, true);
    printScore(1, false);
    printScore(1, true);
    return 0;
}

运行结果如下。

读取失败:编号不能为负数
读取失败:编号超出有效范围
读取失败:成绩数据尚未就绪
读取成功,成绩:85

分析:

在这个例子中,getScore() 针对 3 种不同的失败原因,在前 3 次调用中分别抛出了 std::invalid_argument、std::out_of_range 和 std::runtime_error。直到第 4 次调用传入合法参数,函数才正常返回下标 1 对应的成绩 85。

从上面的代码可以看出,尽管 getScore() 抛出了 3 种不同的异常类型,外层的 printScore() 却只写了一个 catch (const std::exception& e) 分支就把它们全部接住了。这也是实际开发中常用的配合方式:抛出异常时尽量使用具体的子类把错误类别表达清楚,而捕获时如果只需要统一打印提示,直接使用基类 const std::exception& 接收即可。

当然了,如果外层确实需要针对某一种特定错误(比如越界错误)做单独补救,我们也可以编写多个 catch 分支。此时只需要遵循 “子类在前,基类在后” 的顺序,把 std::out_of_range 排在 std::exception 前面就可以了。

try {
    int score = getScore(index, isReady);
    std::cout << "读取成功,成绩:" << score << '\n';
} catch (const std::out_of_range& e) {
    std::cout << "请检查编号范围:" << e.what() << '\n';
} catch (const std::exception& e) {
    std::cout << "其他错误:" << e.what() << '\n';
}

注意 在实际开发中,不要通过判断 e.what() 返回的字符串内容来决定后续业务逻辑。“异常类型” 才是专门用来给 catch 做分支匹配的,而 what() 返回的文字主要是给开发者看日志排错用的。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号