编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

      • README
      • Cpp简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针引用
      • 类和对象
      • 继承多态
      • 内存模型
      • 动态内存
      • IO和文件
      • 异常处理
        • 14.1 异常入门介绍
          • 14.1.1 异常概念解释
          • 14.1.2 运行时异常案例
          • 14.1.3 异常基本语法
          • 14.1.4 捕获异常
          • 14.1.5 发生异常位置
          • 14.1.6 综合案例与思考
        • 14.2 多级catch匹配
          • 14.2.1 多级catch使用
          • 14.2.2 匹配中类型转换
          • 14.2.3 综合案例与思考
        • 14.3 throw抛出异常
          • 14.3.1 异常抛出概念
          • 14.3.2 throw抛异常用法
          • 14.3.3 throw异常规范
          • 14.3.4 综合案例与思考
        • 14.4 exception异常
          • 14.4.1 exception类介绍
          • 14.4.2 exception派生类
          • 14.4.3 定义新的异常
          • 14.4.4 noexcept关键字
          • 14.4.5 综合案例与思考
        • 14.6 异常处理底层原理
          • 14.6.1 异常的零成本模型
          • 14.6.2 栈展开的过程
          • 14.6.3 noexcept的优化意义
        • 14.7 异常处理训练题
        • 14.8 综合思考题
        • 12.9 新手陷阱 Top 5
      • 线程和锁
      • STL模版
      • 预处理器
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 入门教程
杨充
2026-05-07
目录

异常处理

# 第 14 章 C++ 异常处理

# 目录介绍

  • 14.1 异常入门介绍
    • 14.1.1 异常概念解释
    • 14.1.2 运行时异常案例
    • 14.1.3 异常基本语法
    • 14.1.4 捕获异常
    • 14.1.5 发生异常位置
    • 14.1.6 综合案例与思考
  • 14.2 多级catch匹配
    • 14.2.1 多级catch使用
    • 14.2.2 匹配中类型转换
    • 14.2.3 综合案例与思考
  • 14.3 throw抛出异常
    • 14.3.1 异常抛出概念
    • 14.3.2 throw抛异常用法
    • 14.3.3 throw异常规范
    • 14.3.4 综合案例与思考
  • 14.4 exception异常
    • 14.4.1 exception类介绍
    • 14.4.2 exception派生类
    • 14.4.3 定义新的异常
    • 14.4.4 noexcept关键字
    • 14.4.5 综合案例与思考
  • 14.5 异常原理探索
    • 14.5.1 try工作原理
    • 14.5.2 catch工作原理
    • 14.5.3 throw工作原理
  • 14.6 异常处理底层原理
    • 14.6.1 异常的零成本模型
    • 14.6.2 栈展开的过程
    • 14.6.3 noexcept的优化意义
  • 14.7 异常处理训练题
  • 14.8 综合思考题

# 14.1 异常入门介绍

# 14.1.1 异常概念解释

程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:

  1. 语法错误:在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
  2. 逻辑错误:是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
  3. 运行时错误:是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。

运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。

C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。

# 14.1.2 运行时异常案例

一个发生运行时错误的程序:

int main() {
    string str = "https://yccoding.com/";
    char ch1 = str[100];  //下标越界,ch1为垃圾值
    cout << ch1 << endl;
    char ch2 = str.at(100);  //下标越界,抛出异常
    cout << ch2 << endl;
    return 0;
}

运行代码后程序崩溃。崩溃日志如下所示:

ch1
libc++abi: terminating due to uncaught exception of type std::out_of_range: basic_string
Abort trap: 6

分析一下原因:

at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与[ ]不同,at() 会检查下标是否越界,如果越界就抛出一个异常;而[ ]不做检查,不管下标是多少都会照常访问。

所谓抛出异常,就是报告一个运行时错误,程序员可以根据错误信息来进一步处理。

at() 函数检测到下标越界会抛出一个异常,这个异常可以由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,也即终止程序执行。

# 14.1.3 异常基本语法

try {
    // 可能抛出异常的代码
} catch (exception_type &e) {
    // 处理异常
    std::cerr << "Exception caught: " << e.what() << std::endl;
}
  1. try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。
  2. catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。

catch 关键字后面的 exceptionType variable 指明了当前 catch 可以处理的异常类型,以及具体的出错信息。

# 14.1.4 捕获异常

可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:

int main() {
    string str = "https://yccoding.com/";
    try {
        char ch1 = str[100];  //下标越界,ch1为垃圾值
        cout << "ch1" << ch1 << endl;
    } catch (exception e) {
        cout << "exception1 " << e.what() << endl;
    }
    try {
        char ch2 = str.at(100);  //下标越界,抛出异常
        cout << "ch2" << ch2 << endl;
    } catch (exception e) {
        cout << "exception2 " << e.what() << endl;
    }
    return 0;
}

打印结果如下所示:

ch1
exception2 std::exception

结果分析,我们大概可以得出这样的结论:

第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。

第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。

# 14.1.5 发生异常位置

异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。

1.下面的例子演示了 try 块中直接发生的异常:

void test1() {
    try{
        throw "Unknown Exception";  //抛出异常
        cout<<"This statement will not be executed."<<endl;
    }catch(const char* &e){
        cout<<e<<endl;
    }
}

throw关键字用来抛出一个异常,这个异常会被 try 检测到,进而被 catch 捕获。

2.下面的例子演示了 try 块中调用的某个函数中发生了异常:

void func() {
    throw "Unknown Exception";  //抛出异常
    cout << "[1]This statement will not be executed." << endl;
}

void test2() {
    try {
        func();
        cout << "[2]This statement will not be executed." << endl;
    } catch (const char *&e) {
        cout << e << endl;
    }
}

func() 在 try 块中被调用,它抛出的异常会被 try 检测到,进而被 catch 捕获。从运行结果可以看出,func() 中的 cout 和 try 中的 cout 都没有被执行。

3.try 块中调用了某个函数,该函数又调用了另外的一个函数,这个另外的函数抛出了异常:

void func_inner() {
    throw "Unknown Exception";  //抛出异常
    cout << "[1]This statement will not be executed." << endl;
}

void func_outer() {
    func_inner();
    cout << "[2]This statement will not be executed." << endl;
}

void test3() {
    try {
        func_outer();
        cout << "[3]This statement will not be executed." << endl;
    } catch (const char *&e) {
        cout << e << endl;
    }
}

发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过,没有执行的机会了。

# 14.1.6 综合案例与思考

下面通过一个"安全除法计算器"案例,综合演示异常的概念、基本语法、捕获方式以及异常在调用链中的传播:

#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;

// 解析字符串为整数(可能抛出异常)
int parseInput(const string& input) {
    // 空字符串检查
    if (input.empty()) {
        throw string("输入为空");
    }
    // 使用 stoi 转换,可能抛出 invalid_argument 或 out_of_range
    return stoi(input);
}

// 安全除法(可能抛出异常)
double safeDivide(int a, int b) {
    if (b == 0) {
        throw runtime_error("除数不能为零");
    }
    return static_cast<double>(a) / b;
}

// 计算入口——异常沿调用链传播
double calculate(const string& numStr, const string& denStr) {
    int num = parseInput(numStr);    // 可能抛出 string / invalid_argument / out_of_range
    int den = parseInput(denStr);    // 同上
    return safeDivide(num, den);     // 可能抛出 runtime_error
}

int main() {
    // 测试 1:正常计算
    try {
        cout << "10 / 3 = " << calculate("10", "3") << endl;
    } catch (...) {
        cout << "不应到达此处" << endl;
    }

    // 测试 2:除数为零
    try {
        calculate("10", "0");
    } catch (const runtime_error& e) {
        cout << "捕获 runtime_error: " << e.what() << endl;
    }

    // 测试 3:输入无效字符串
    try {
        calculate("abc", "5");
    } catch (const invalid_argument& e) {
        cout << "捕获 invalid_argument: " << e.what() << endl;
    }

    // 测试 4:空字符串——异常从 parseInput 沿调用链传播到 main
    try {
        calculate("", "5");
    } catch (const string& e) {
        cout << "捕获 string 异常: " << e << endl;
    }

    // 测试 5:数值溢出
    try {
        calculate("99999999999999999999", "1");
    } catch (const out_of_range& e) {
        cout << "捕获 out_of_range: " << e.what() << endl;
    }

    cout << "程序正常结束" << endl;
    return 0;
}

输出结果:

10 / 3 = 3.33333
捕获 runtime_error: 除数不能为零
捕获 invalid_argument: stoi
捕获 string 异常: 输入为空
捕获 out_of_range: stoi
程序正常结束

案例知识融合:本案例将三种错误类型(语法/逻辑/运行时)的区分、try-catch 基本语法、异常捕获与处理、以及异常沿函数调用链的传播机制整合到一个实际场景中。safeDivide 和 parseInput 分别在不同层级抛出异常,calculate 不处理异常而是让其继续向上传播,最终由 main 中的 try-catch 统一捕获,完整体现了"抛出→检测→捕获"的流程。

思考题:

  1. 如果在 calculate 函数中也加一层 try-catch 并重新抛出异常(throw;),对外部的调用者有什么影响?
  2. 如果把 catch (const string& e) 改成 catch (string e)(值捕获),是否仍能正常工作?两者有什么区别?
  3. 五个测试用例中,如果去掉所有 try-catch,程序会在哪个测试点崩溃?为什么后面的测试不会执行?

# 14.2 多级catch匹配

# 14.2.1 多级catch使用

一个 try 对应一个 catch,这只是最简单的形式。其实,一个 try 后面可以跟多个 catch:

try{
    //可能抛出异常的语句
}catch (exception_type_1 e){
    //处理异常的语句
}catch (exception_type_2 e){
    //处理异常的语句
}
//其他的catch
catch (exception_type_n e){
    //处理异常的语句
}

当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。

演示了多级 catch 的使用:

class Base {
};

class Derived : public Base {
};

int main() {
    try {
        throw Derived();  //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
        cout << "This statement will not be executed." << endl;
    } catch (int) {
        cout << "Exception type: int" << endl;
    } catch (char *) {
        cout << "Exception type: cahr *" << endl;
    } catch (Base) {  //匹配成功(向上转型)
        cout << "Exception type: Base" << endl;
    } catch (Derived) {
        cout << "Exception type: Derived" << endl;
    }
    return 0;
}

在 catch 中,我们只给出了异常类型,没有给出接收异常信息的变量。

# 14.2.2 匹配中类型转换

C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:

  1. 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
  2. 向上转型:也就是派生类向基类的转换。
  3. const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
  4. 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
  5. 用户自定的类型转换。

catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。

演示了 const 转换以及数组和指针的转换:

int main(){
    int nums[] = {1, 2, 3};
    try {
        throw nums;
        cout << "This statement will not be executed." << endl;
    } catch (const int *) {
        cout << "Exception type: const int *" << endl;
    }
    return 0;
}

运行结果如下所示:

Exception type: const int *

# 14.2.3 综合案例与思考

下面通过一个"文件解析器"案例,综合演示多级 catch 匹配、类型转换规则以及 catch-all 的使用:

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// 异常层次结构
class ParseError : public runtime_error {
public:
    ParseError(const string& msg) : runtime_error(msg) {}
};

class SyntaxError : public ParseError {
public:
    int line;
    SyntaxError(const string& msg, int ln)
        : ParseError(msg), line(ln) {}
};

class SemanticError : public ParseError {
public:
    SemanticError(const string& msg) : ParseError(msg) {}
};

// 模拟文件解析,根据 mode 抛出不同类型的异常
void parseFile(int mode) {
    switch (mode) {
        case 1: throw SyntaxError("缺少分号", 42);
        case 2: throw SemanticError("变量未定义");
        case 3: throw runtime_error("文件损坏");
        case 4: throw 404;                        // 抛出 int 类型
        case 5: throw "未知格式";                   // 抛出 const char*
        default: cout << "解析成功" << endl;
    }
}

void testMultiCatch(int mode) {
    cout << "\n--- 测试 mode=" << mode << " ---" << endl;
    try {
        parseFile(mode);
    }
    // 注意:派生类 catch 必须放在基类前面,否则永远不会被匹配
    catch (const SyntaxError& e) {
        cout << "[SyntaxError] 第" << e.line << "行: " << e.what() << endl;
    }
    catch (const SemanticError& e) {
        cout << "[SemanticError] " << e.what() << endl;
    }
    catch (const ParseError& e) {
        // 不会匹配到 SyntaxError/SemanticError,因为它们已被前面的 catch 捕获
        cout << "[ParseError] " << e.what() << endl;
    }
    catch (const runtime_error& e) {
        cout << "[runtime_error] " << e.what() << endl;
    }
    catch (int code) {
        cout << "[int] 错误码: " << code << endl;
    }
    catch (const char* msg) {
        // const 转换:const char* 匹配 const char*
        cout << "[const char*] " << msg << endl;
    }
    catch (...) {
        // catch-all:捕获所有未匹配的异常
        cout << "[...] 未知异常类型" << endl;
    }
}

int main() {
    for (int i = 0; i <= 5; ++i) {
        testMultiCatch(i);
    }
    return 0;
}

输出结果:

--- 测试 mode=0 ---
解析成功

--- 测试 mode=1 ---
[SyntaxError] 第42行: 缺少分号

--- 测试 mode=2 ---
[SemanticError] 变量未定义

--- 测试 mode=3 ---
[runtime_error] 文件损坏

--- 测试 mode=4 ---
[int] 错误码: 404

--- 测试 mode=5 ---
[const char*] 未知格式

案例知识融合:本案例演示了多级 catch 的匹配顺序——编译器从上到下依次尝试匹配,一旦找到就停止。SyntaxError 和 SemanticError 都是 ParseError 的派生类,由于"向上转型"规则,如果将 catch(ParseError) 放在前面,派生类的 catch 将永远不会被执行。同时展示了 catch 匹配中仅支持向上转型、const 转换和数组/指针转换三种类型转换,int 不会转换为 double 来匹配。catch(...) 作为兜底捕获所有未匹配的异常类型。

思考题:

  1. 如果将 catch (const ParseError& e) 移动到 catch (const SyntaxError& e) 前面,SyntaxError 异常还能被正确识别吗?为什么?
  2. 如果抛出的是 double 类型(如 throw 3.14;),上面的 catch(int) 能否捕获?为什么?
  3. 在实际项目中,catch(...) 放在最后有什么好处?如果只写一个 catch(...) 不写其他 catch 会有什么问题?

# 14.3 throw抛出异常

# 14.3.1 异常抛出概念

C++ 异常处理的流程,具体为:抛出(Throw)--> 检测(Try) --> 捕获(Catch)

异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。

# 14.3.2 throw抛异常用法

throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。

抛出异常:使用throw关键字后跟一个表达式,将异常抛出。这个表达式可以是任何类型,通常是一个异常类的对象。

void func() {
    int num;
    cout << "请输入整型变量:" << endl;
    cin >> num;
    if (num == 0) {
        throw "抛出异常";
    }
}

int main(){
    try {
        func();
        cout << "看看这段代码是否执行"<< endl;
    } catch (exception e) {
        cout << "异常:" << e.what() << endl;
    }
    return 0;
}

测试如下所示:

0
libc++abi: terminating due to uncaught exception of type char const*
Abort trap: 6

# 14.3.3 throw异常规范

虚函数中的异常规范

C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。

只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。请看下面的例子:

class Base{
public:
    virtual int fun1(int) throw();
    virtual int fun2(int) throw(int);
    virtual string fun3() throw(int, string);
};
class Derived:public Base{
public:
    int fun1(int) throw(int);   //错!异常规范不如 throw() 严格
    int fun2(int) throw(int);   //对!有相同的异常规范
    string fun3() throw(string);  //对!异常规范比 throw(int,string) 更严格
}

异常规范与函数定义和函数声明

C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。

//错!定义中有异常规范,声明中没有
void func1();
void func1() throw(int) { }

//错!定义和声明中的异常规范不一致
void func2() throw(int);
void func2() throw(int, bool) { }

//对!定义和声明中的异常规范严格一致
void func3() throw(float, char*);
void func3() throw(float, char*) { }

# 14.3.4 综合案例与思考

下面通过一个"配置加载器"案例,综合演示 throw 抛出异常、异常规范以及重新抛出异常的用法:

#include <iostream>
#include <string>
#include <stdexcept>
#include <map>
using namespace std;

// 自定义配置异常
class ConfigError : public runtime_error {
    string key_;
public:
    ConfigError(const string& key, const string& msg)
        : runtime_error("配置项[" + key + "]: " + msg), key_(key) {}
    const string& getKey() const { return key_; }
};

class ConfigLoader {
    map<string, string> config_;
public:
    // 加载配置(模拟)
    void load(const string& filename) {
        if (filename.empty()) {
            throw invalid_argument("文件名不能为空");  // 抛出标准异常
        }
        // 模拟加载一些配置
        config_["port"] = "8080";
        config_["host"] = "localhost";
        config_["timeout"] = "-5";     // 故意设置无效值
        config_["max_conn"] = "abc";   // 故意设置无效值
        cout << "配置文件 [" << filename << "] 加载完成" << endl;
    }

    // 获取整数配置值
    int getInt(const string& key) {
        auto it = config_.find(key);
        if (it == config_.end()) {
            throw ConfigError(key, "配置项不存在");  // 抛出自定义异常
        }
        int value;
        try {
            value = stoi(it->second);
        } catch (const invalid_argument&) {
            // 捕获后重新抛出更有意义的异常
            throw ConfigError(key, "值 '" + it->second + "' 不是有效整数");
        }
        if (value < 0) {
            throw ConfigError(key, "值不能为负数: " + to_string(value));
        }
        return value;
    }

    // 验证所有配置——捕获并记录后重新抛出
    void validate() {
        try {
            int port = getInt("port");
            cout << "port = " << port << " ✓" << endl;

            int timeout = getInt("timeout");
            cout << "timeout = " << timeout << " ✓" << endl;
        } catch (const ConfigError& e) {
            cerr << "验证失败: " << e.what() << endl;
            throw;  // 重新抛出当前异常,保留原始类型
        }
    }
};

int main() {
    ConfigLoader loader;

    // 测试 1:空文件名
    try {
        loader.load("");
    } catch (const invalid_argument& e) {
        cout << "捕获: " << e.what() << endl;
    }

    // 测试 2:正常加载 + 验证失败(timeout 为负数)
    try {
        loader.load("app.conf");
        loader.validate();
    } catch (const ConfigError& e) {
        cout << "外层捕获: " << e.what() << endl;
        cout << "问题配置项: " << e.getKey() << endl;
    }

    // 测试 3:获取不存在的配置
    try {
        loader.getInt("database_port");
    } catch (const ConfigError& e) {
        cout << "捕获: " << e.what() << endl;
    }

    // 测试 4:获取非法字符串配置
    try {
        loader.getInt("max_conn");
    } catch (const ConfigError& e) {
        cout << "捕获: " << e.what() << endl;
    }

    cout << "程序正常结束" << endl;
    return 0;
}

输出结果:

捕获: 文件名不能为空
配置文件 [app.conf] 加载完成
port = 8080 ✓
验证失败: 配置项[timeout]: 值不能为负数: -5
外层捕获: 配置项[timeout]: 值不能为负数: -5
问题配置项: timeout
捕获: 配置项[database_port]: 配置项不存在
捕获: 配置项[max_conn]: 值 'abc' 不是有效整数
程序正常结束

案例知识融合:本案例完整演示了 throw 的三种典型用法:直接抛出标准异常(throw invalid_argument)、抛出自定义异常对象(throw ConfigError)、以及捕获后重新抛出(throw;)。getInt 中捕获底层的 invalid_argument 后包装成更有业务含义的 ConfigError 再抛出,体现了异常转译的最佳实践。validate 中使用 throw; 重新抛出异常,保留了原始异常类型和信息,让外层调用者也能处理。

思考题:

  1. validate 中如果把 throw; 改成 throw e;,异常类型会有什么变化?(提示:考虑对象切片问题)
  2. 为什么 getInt 中选择捕获 invalid_argument 并转换为 ConfigError,而不是直接让 invalid_argument 传播出去?这样做的好处是什么?
  3. 如果在 load 函数中添加 noexcept 修饰,当传入空文件名时会发生什么?

# 14.4 exception异常

# 14.4.1 exception类介绍

exception 类位于 头文件中,它被声明为:

class exception{
public:
    exception () throw();  //构造函数
    exception (const exception&) throw();  //拷贝构造函数
    exception& operator= (const exception&) throw();  //运算符重载
    virtual ~exception() throw();  //虚析构函数
    virtual const char* what() const throw();  //虚函数
}

这里需要说明的是 what() 函数。what() 函数返回一个能识别异常的字符串,正如它的名字“what”一样,可以粗略地告诉你这是什么异常。不过C++标准并没有规定这个字符串的格式,各个编译器的实现也不同,所以 what() 的返回值仅供参考。

# 14.4.2 exception派生类

先来看一下 exception 类的直接派生类:

logic_error 逻辑错误。

  1. runtime_error 运行时错误。
  2. bad_alloc 使用 new 或 new[ ] 分配内存失败时抛出的异常。
  3. bad_typeid 使用 typeid 操作一个 NULL 指针,而且该指针是带有虚函数的类,这时抛出 bad_typeid 异常。
  4. bad_cast 使用 dynamic_cast 转换失败时抛出的异常。
  5. ios_base::failure io 过程中出现的异常。
  6. bad_exception 这是个特殊的异常,如果函数的异常列表里声明了 bad_exception 异常,当函数内部抛出了异常列表中没有的异常时,如果调用的 unexpected() 函数中抛出了异常,不论什么类型,都会被替换为 bad_exception 类型。

logic_error 的派生类:

  1. length_error,试图生成一个超出该类型最大长度的对象时抛出该异常,例如 vector 的 resize 操作。
  2. domain_error,参数的值域错误,主要用在数学函数中,例如使用一个负值调用只能操作非负数的函数。
  3. out_of_range,超出有效范围。
  4. invalid_argument,参数不合适。在标准库中,当利用string对象构造 bitset 时,而 string 中的字符不是 0 或1 的时候,抛出该异常

runtime_error 的派生类:

  1. range_error,当尝试存储超出范围的值时,会抛出该异常。
  2. overflow_error,当发生数学上溢时,会抛出该异常。
  3. underflow_error,当发生数学下溢时,会抛出该异常。

# 14.4.3 定义新的异常

创建异常类:创建一个新的类来表示您的异常。通常,您的异常类应该继承自std::exception或其派生类,以符合C++异常处理的标准。

struct MyException : public exception {
    //const throw() 不是函数,这个东西叫异常规格说明,表示 what 函数可以抛出异常的类型,类型说明放到 () 里,
    //这里面没有类型,就是声明这个函数不抛出异常,通常函数不写后面的 throw() 就表示函数可以抛出任何类型的异常。
    const char *what() const throw() {
        return "custom c++ exception";
    }
};

然后使用自定义异常

//您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:
void test() {
    try {
        throw MyException();
    }
    catch (MyException &e) {
        std::cout << "MyException caught" << std::endl;
        std::cout << e.what() << std::endl;
    }
    catch (std::exception &e) {
        std::cout << "其他的错误" << std::endl;
    }
}

int main() {
    test();
    return 0;
}

# 14.4.4 noexcept关键字

noexcept 用于指示函数不会抛出异常。如果标记为 noexcept 的函数抛出了异常,程序会直接终止。

#include <iostream>

void safeFunction() noexcept {
    std::cout << "This function is safe!\n";
}

int main() {
    safeFunction();
    return 0;
}

# 14.4.5 综合案例与思考

下面通过一个"银行转账系统"案例,综合演示 exception 类层次、自定义异常、noexcept 关键字的用法:

#include <iostream>
#include <string>
#include <stdexcept>
#include <vector>
using namespace std;

// 自定义异常层次——继承自 runtime_error(exception 的派生类)
class BankException : public runtime_error {
public:
    BankException(const string& msg) : runtime_error(msg) {}
};

class InsufficientFundsError : public BankException {
    double shortage_;
public:
    InsufficientFundsError(double shortage)
        : BankException("余额不足,差额: " + to_string(shortage)),
          shortage_(shortage) {}
    double getShortage() const noexcept { return shortage_; }
};

class AccountNotFoundError : public BankException {
    string accountId_;
public:
    AccountNotFoundError(const string& id)
        : BankException("账户不存在: " + id), accountId_(id) {}
    const string& getAccountId() const noexcept { return accountId_; }
};

class InvalidAmountError : public BankException {
public:
    InvalidAmountError(double amount)
        : BankException("无效金额: " + to_string(amount)) {}
};

// 银行账户
class Account {
    string id_;
    double balance_;
public:
    Account(const string& id, double balance) : id_(id), balance_(balance) {}

    const string& getId() const noexcept { return id_; }
    double getBalance() const noexcept { return balance_; }  // noexcept:保证不抛异常

    void deposit(double amount) {
        if (amount <= 0) throw InvalidAmountError(amount);
        balance_ += amount;
    }

    void withdraw(double amount) {
        if (amount <= 0) throw InvalidAmountError(amount);
        if (amount > balance_) throw InsufficientFundsError(amount - balance_);
        balance_ -= amount;
    }
};

// 银行系统
class Bank {
    vector<Account> accounts_;
public:
    void addAccount(const string& id, double balance) noexcept {
        accounts_.emplace_back(id, balance);
    }

    Account& findAccount(const string& id) {
        for (auto& acc : accounts_) {
            if (acc.getId() == id) return acc;
        }
        throw AccountNotFoundError(id);
    }

    // 转账操作
    void transfer(const string& fromId, const string& toId, double amount) {
        Account& from = findAccount(fromId);
        Account& to = findAccount(toId);
        from.withdraw(amount);  // 可能抛出 InsufficientFundsError 或 InvalidAmountError
        to.deposit(amount);
    }
};

// 辅助函数:打印账户信息
void printBalance(Bank& bank, const string& id) noexcept {
    try {
        Account& acc = bank.findAccount(id);
        cout << "  账户[" << id << "] 余额: " << acc.getBalance() << endl;
    } catch (const AccountNotFoundError& e) {
        cout << "  " << e.what() << endl;
    }
}

int main() {
    Bank bank;
    bank.addAccount("A001", 1000.0);
    bank.addAccount("A002", 500.0);

    cout << "=== 初始状态 ===" << endl;
    printBalance(bank, "A001");
    printBalance(bank, "A002");

    // 测试 1:正常转账
    cout << "\n=== 转账 200 从 A001 到 A002 ===" << endl;
    try {
        bank.transfer("A001", "A002", 200);
        cout << "转账成功" << endl;
    } catch (const BankException& e) {
        cout << "转账失败: " << e.what() << endl;
    }
    printBalance(bank, "A001");
    printBalance(bank, "A002");

    // 测试 2:余额不足
    cout << "\n=== 转账 5000 从 A001 到 A002 ===" << endl;
    try {
        bank.transfer("A001", "A002", 5000);
    } catch (const InsufficientFundsError& e) {
        cout << "转账失败: " << e.what() << endl;
        cout << "差额: " << e.getShortage() << endl;
    }

    // 测试 3:账户不存在
    cout << "\n=== 转账到不存在的账户 ===" << endl;
    try {
        bank.transfer("A001", "A999", 100);
    } catch (const AccountNotFoundError& e) {
        cout << "转账失败: " << e.what() << endl;
    }

    // 测试 4:无效金额
    cout << "\n=== 转账负数金额 ===" << endl;
    try {
        bank.transfer("A001", "A002", -50);
    } catch (const InvalidAmountError& e) {
        cout << "转账失败: " << e.what() << endl;
    }

    // 测试 5:用基类 exception 捕获所有异常
    cout << "\n=== 使用 exception 基类捕获 ===" << endl;
    try {
        bank.transfer("A001", "A002", 9999);
    } catch (const exception& e) {
        cout << "捕获到异常: " << e.what() << endl;
    }

    cout << "\n=== 最终状态 ===" << endl;
    printBalance(bank, "A001");
    printBalance(bank, "A002");
    return 0;
}

输出结果:

=== 初始状态 ===
  账户[A001] 余额: 1000
  账户[A002] 余额: 500

=== 转账 200 从 A001 到 A002 ===
转账成功
  账户[A001] 余额: 800
  账户[A002] 余额: 700

=== 转账 5000 从 A001 到 A002 ===
转账失败: 余额不足,差额: 4200.000000
差额: 4200

=== 转账到不存在的账户 ===
转账失败: 账户不存在: A999

=== 转账负数金额 ===
转账失败: 无效金额: -50.000000

=== 使用 exception 基类捕获 ===
捕获到异常: 余额不足,差额: 7199.000000

=== 最终状态 ===
  账户[A001] 余额: 800
  账户[A002] 余额: 700

案例知识融合:本案例构建了完整的自定义异常层次体系(BankException → InsufficientFundsError / AccountNotFoundError / InvalidAmountError),全部继承自 runtime_error(exception 的派生类),通过重写 what() 传递错误信息。noexcept 用于标记确保不抛异常的函数(如 getBalance、getId、printBalance),使编译器能进行优化。测试5演示了用 exception 基类引用捕获所有标准异常派生类,体现了多态机制在异常处理中的应用。

思考题:

  1. transfer 中如果 withdraw 成功但 deposit 失败,会出现什么问题?如何实现异常安全的转账操作(提示:先检查再操作,或捕获异常后回滚)?
  2. noexcept 标记的函数如果意外抛出异常会怎样?为什么 printBalance 内部需要 try-catch 来保证 noexcept 承诺?
  3. 为什么自定义异常建议继承 runtime_error 而不是直接继承 exception?两种方式有什么区别?

# 14.6 异常处理底层原理

# 14.6.1 异常的零成本模型

现代C++编译器(GCC、Clang)使用零成本异常模型(Zero-Cost Exception):

  • 不抛异常时:零运行时开销——不执行任何额外指令,不检查任何标志
  • 抛异常时:非常昂贵——需要查表、栈展开、对象析构

这种设计基于一个假设:异常是罕见的异常情况,正常路径不应为其付出代价。

实现机制——表驱动(Table-Driven):

编译器为每个函数生成一张异常处理表,存储在.gcc_except_table段中:

函数foo的异常表:
+------------------+------------------+------------------+
| 代码地址范围       | landing pad地址   | 捕获的异常类型     |
| [0x4010, 0x4050) | 0x4060           | std::runtime_error|
| [0x4050, 0x4080) | 0x4090           | std::exception   |
+------------------+------------------+------------------+

当异常抛出时,运行时库查找这张表,确定:

  1. 当前位置是否在某个try块中
  2. 对应的catch块(landing pad)在哪里
  3. 需要析构哪些局部对象

# 14.6.2 栈展开的过程

throw抛出异常后的完整流程:

void c() { throw runtime_error("error"); }
void b() { string s = "hello"; c(); }  // s需要被析构
void a() {
    try { b(); }
    catch (exception& e) { cout << e.what(); }
}

执行过程:

1. c()中执行throw
   → 调用__cxa_allocate_exception分配异常对象
   → 调用__cxa_throw启动栈展开

2. 栈展开(Stack Unwinding):
   → 查c()的异常表:没有catch → 继续展开
   → 进入b()的清理阶段:调用s的析构函数(~string())
   → 查b()的异常表:没有catch → 继续展开
   → 查a()的异常表:找到匹配的catch块

3. 跳转到a()的catch块
   → 执行catch中的代码
   → 调用__cxa_end_catch释放异常对象

关键点:栈展开过程中,所有已构造的局部对象都会被正确析构。这就是RAII在异常安全中的核心价值——即使发生异常,资源也不会泄漏。

# 14.6.3 noexcept的优化意义

noexcept告诉编译器"这个函数不会抛异常":

void safe() noexcept { /* ... */ }
void risky() { /* 可能抛异常 */ }

noexcept带来的优化:

  1. 不生成异常处理表:减少二进制大小
  2. 编译器可以做更激进的优化:知道不会异常中断,可以更自由地重排指令
  3. STL容器优化:vector扩容时,如果元素的移动构造函数是noexcept的,使用移动语义;否则退回到拷贝(保证强异常安全)
// vector扩容的决策逻辑(伪代码)
if (is_nothrow_move_constructible<T>::value) {
    // 移动元素(快,但如果抛异常则数据丢失)
    move(old_begin, old_end, new_begin);
} else {
    // 拷贝元素(慢,但异常安全)
    copy(old_begin, old_end, new_begin);
}

noexcept违约的后果:如果noexcept函数实际抛出了异常,程序立即调用std::terminate()终止,不进行栈展开。

# 14.7 异常处理训练题

训练题1:构建异常安全的栈

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

template<typename T>
class SafeStack {
    T* data_;
    int top_;
    int capacity_;

public:
    class StackEmpty : public runtime_error {
    public:
        StackEmpty() : runtime_error("栈为空") {}
    };

    class StackFull : public runtime_error {
    public:
        StackFull() : runtime_error("栈已满") {}
    };

    explicit SafeStack(int cap = 10)
        : data_(new T[cap]), top_(-1), capacity_(cap) {}

    ~SafeStack() { delete[] data_; }

    void push(const T& val) {
        if (top_ >= capacity_ - 1) throw StackFull();
        data_[++top_] = val;
    }

    T pop() {
        if (top_ < 0) throw StackEmpty();
        return data_[top_--];
    }

    const T& peek() const {
        if (top_ < 0) throw StackEmpty();
        return data_[top_];
    }

    bool empty() const noexcept { return top_ < 0; }
    int size() const noexcept { return top_ + 1; }
};

int main() {
    SafeStack<int> stack(3);

    try {
        stack.push(10);
        stack.push(20);
        stack.push(30);
        cout << "栈顶: " << stack.peek() << endl;

        stack.push(40);  // 触发StackFull
    } catch (const SafeStack<int>::StackFull& e) {
        cout << "捕获异常: " << e.what() << endl;
    }

    try {
        while (!stack.empty()) {
            cout << "弹出: " << stack.pop() << endl;
        }
        stack.pop();  // 触发StackEmpty
    } catch (const SafeStack<int>::StackEmpty& e) {
        cout << "捕获异常: " << e.what() << endl;
    }

    return 0;
}

练习重点:自定义异常类继承runtime_error、RAII管理内存、noexcept标记不抛异常的函数。


训练题2:异常安全的文件事务

#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
using namespace std;

class FileTransaction {
    string path_;
    string tempPath_;
    string backup_;
    bool committed_;

public:
    FileTransaction(const string& path)
        : path_(path), tempPath_(path + ".tmp"),
          backup_(path + ".bak"), committed_(false) {}

    ~FileTransaction() {
        if (!committed_) {
            // 回滚:删除临时文件
            remove(tempPath_.c_str());
            cout << "[回滚] 临时文件已清理" << endl;
        }
    }

    ofstream begin() {
        // 备份原文件
        ifstream src(path_);
        if (src.good()) {
            ofstream bak(backup_);
            bak << src.rdbuf();
        }
        return ofstream(tempPath_);
    }

    void commit() {
        // 原子性替换:先删原文件,再重命名临时文件
        remove(path_.c_str());
        if (rename(tempPath_.c_str(), path_.c_str()) != 0) {
            // 恢复备份
            rename(backup_.c_str(), path_.c_str());
            throw runtime_error("提交失败,已恢复备份");
        }
        remove(backup_.c_str());
        committed_ = true;
        cout << "[提交] 文件更新成功" << endl;
    }
};

int main() {
    // 成功的事务
    try {
        FileTransaction tx("data.txt");
        auto out = tx.begin();
        out << "新的文件内容" << endl;
        out << "第二行数据" << endl;
        out.close();
        tx.commit();
    } catch (const exception& e) {
        cout << "错误: " << e.what() << endl;
    }

    // 模拟失败的事务(析构时自动回滚)
    try {
        FileTransaction tx("data.txt");
        auto out = tx.begin();
        out << "这次修改会被回滚" << endl;
        out.close();
        throw runtime_error("模拟写入失败");
        // tx.commit() 不会被调用
    } catch (const exception& e) {
        cout << "错误: " << e.what() << endl;
    }
    // tx析构 → 自动回滚

    return 0;
}

练习重点:RAII实现自动回滚、异常安全的事务模式、析构函数做清理工作。

# 14.8 综合思考题

  1. 异常 vs 错误码:C使用返回错误码(如errno),C++提供异常机制。Google C++风格指南曾禁止使用异常。请对比两种方式的优缺点。在什么场景下应该使用异常?什么场景下错误码更合适?C++23的std::expected<T,E>如何结合了两者的优点?

  2. 异常安全的三个级别:基本保证(不泄漏资源)、强保证(要么成功要么回滚到原状态)、不抛保证(承诺不抛异常)。STL容器的各种操作分别提供了哪个级别的保证?push_back在扩容时如何确保强异常安全?

  3. catch(...)的陷阱:catch(...)可以捕获所有异常,但你无法获取异常信息。在析构函数中,如果需要调用可能抛异常的清理代码,应该如何处理?为什么C++11默认析构函数是noexcept的?

# 12.9 新手陷阱 Top 5

# 陷阱 说明
1 析构函数抛异常 直接 std::terminate;析构默认 noexcept
2 标了 noexcept 却抛异常 直接 std::terminate,不会被外层 catch 捕获
3 catch (Exception e) 切片 多态异常被切成基类;用 catch (const Exception& e)
4 通过异常做正常控制流 异常成本高,不要当 goto 用
5 跨 DLL/SO 抛 C++ 异常 行为未定义;接口处转错误码
上次更新: 2026/06/28, 17:55:19
IO和文件
线程和锁

← IO和文件 线程和锁→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式