编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • 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;
}
1
2
3
4
5
6
7
8

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

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

分析一下原因:

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

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

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

# 14.1.3 异常基本语法

try {
    // 可能抛出异常的代码
} catch (exception_type &e) {
    // 处理异常
    std::cerr << "Exception caught: " << e.what() << std::endl;
}
1
2
3
4
5
6
  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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

打印结果如下所示:

ch1
exception2 std::exception
1
2

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

第一个 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;
    }
}
1
2
3
4
5
6
7
8

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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

输出结果:

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

案例知识融合:本案例将三种错误类型(语法/逻辑/运行时)的区分、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){
    //处理异常的语句
}
1
2
3
4
5
6
7
8
9
10
11

当异常发生时,程序会按照从上到下的顺序,将异常类型和 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在 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;
}
1
2
3
4
5
6
7
8
9
10

运行结果如下所示:

Exception type: const int *
1

# 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

输出结果:

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

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

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

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

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

--- 测试 mode=5 ---
[const char*] 未知格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

案例知识融合:本案例演示了多级 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

测试如下所示:

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

# 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) 更严格
}
1
2
3
4
5
6
7
8
9
10
11
12

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

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*) { }
1
2
3
4
5
6
7
8
9
10
11

# 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

输出结果:

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

案例知识融合:本案例完整演示了 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();  //虚函数
}
1
2
3
4
5
6
7
8

这里需要说明的是 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";
    }
};
1
2
3
4
5
6
7

然后使用自定义异常

//您可以通过继承和重载 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 14.4.4 noexcept关键字

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

#include <iostream>

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

int main() {
    safeFunction();
    return 0;
}
1
2
3
4
5
6
7
8
9
10

# 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

输出结果:

=== 初始状态 ===
  账户[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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

案例知识融合:本案例构建了完整的自定义异常层次体系(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
2
3
4
5
6

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

  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
2
3
4
5
6

执行过程:

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释放异常对象
1
2
3
4
5
6
7
8
9
10
11
12
13

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

# 14.6.3 noexcept的优化意义

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

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

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);
}
1
2
3
4
5
6
7
8

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

练习重点:自定义异常类继承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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

练习重点: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/10, 11:13:41
IO和文件
线程和锁

← IO和文件 线程和锁→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式