Skip to content

第9章 异常处理 ​


🎯 本章学习目标 ​

通过本章的学习,你将能够:

  1. 理解异常概念:知道异常是程序运行时的非正常情况,区别于编译错误
  2. 掌握try-throw-catch:熟练使用异常处理三件套的基本语法
  3. 理解执行流程:清楚 throw 后 try 中代码被跳过,直接跳转到匹配的 catch
  4. 掌握多catch匹配:理解多个 catch 块按顺序匹配的规则
  5. 了解异常传播:知道异常会沿调用栈向上传播,直到被捕获
  6. 使用catch(...):知道"..."可以捕获所有类型的异常,但必须放最后
  7. 了解标准异常:知道 std::exception 是基类,what() 方法获取描述
  8. 区分异常类型:能判断抛出和捕获的类型是否匹配
  9. 理解noexcept:知道 noexcept 声明函数不抛出异常的含义
  10. 阅读异常代码:能快速分析异常处理代码的输出结果

🗺️ 本章知识图谱 ​

异常处理
├── 基础概念
│   ├── 异常的定义(运行时非正常情况)
│   ├── 编译错误 vs 运行时错误
│   ├── 异常处理 vs 错误码
│   └── 常见异常类型(除零、越界、文件失败等)
├── 基本语法
│   ├── try 块(保护代码)
│   ├── throw 抛出异常
│   ├── catch 捕获异常
│   └── 执行流程(throw后跳过try剩余代码)
├── 多 catch 匹配
│   ├── 类型匹配规则
│   ├── 从具体到通用排列
│   ├── 父类引用捕获子类
│   └── catch(...) 兜底(放最后)
├── 异常传播
│   ├── 沿调用栈向上传播
│   ├── 栈展开
│   ├── 未捕获异常 → terminate
│   └── 类型不匹配 → 继续向上找
├── 标准异常
│   ├── std::exception 基类
│   ├── what() 方法
│   ├── std::runtime_error
│   ├── std::logic_error
│   └── std::bad_alloc
├── noexcept
│   ├── 声明函数不抛异常
│   └── 抛出则直接 terminate
└── 常见陷阱
    ├── try 后没有 catch(编译错误)
    ├── catch(...) 放最前面(后面不可达)
    ├── 异常类型不匹配(无法捕获)
    └── throw 后代码不会执行

一、什么是异常 ​

异常(Exception)是程序在运行时出现的非正常情况,它会打断程序的正常执行流程。常见的异常包括:

异常类型典型场景不处理的后果
除零错误1 / 0、a % 0程序崩溃(浮点异常)
数组越界访问 arr[-1]、arr[100](数组长度为10)读写非法内存,结果不可预期
文件打开失败打开不存在的文件、无权限后续读写操作全部失败
内存分配失败new 时内存不足抛出 std::bad_alloc,未捕获则终止
输入非法用户输入字母而非数字数据错误,逻辑异常
cpp
#include <iostream>
using namespace std;

int main() {
    int a = 10, b = 0;
    cout << a / b << endl;  // 编译通过!运行时崩溃:Floating point exception
    return 0;
}

⚠️ 易错点: 异常 ≠ 语法错误。语法错误在编译阶段就被编译器发现,程序根本无法运行;异常发生在运行阶段,编译时完全正常,执行时才出问题。考试常以判断题形式考查这一区别。


二、为什么需要异常处理 ​

在没有异常机制的时代,程序员通常通过返回错误码来处理错误:

cpp
#include <iostream>
using namespace std;

// 传统错误码方式:通过引用参数返回结果,函数返回值表示错误码
int divide(int a, int b, int& result) {
    if (b == 0) return -1;   // -1 表示出错
    result = a / b;
    return 0;                // 0 表示成功
}

int main() {
    int r;
    int err = divide(10, 0, r);
    if (err == -1) {              // 必须手动检查返回值
        cout << "除数不能为零!" << endl;
        return -1;
    }
    cout << r << endl;
    return 0;
}

这种方式能工作,但有明显弊端。异常处理带来了三大改进:

对比维度错误码返回异常处理(try-catch-throw)
代码可读性错误判断穿插在业务逻辑中,容易遗漏正常逻辑写在 try,错误处理写在 catch,职责分离
错误传播每层函数都要手动检查并向上传递错误码异常自动沿调用栈向上传播,无需层层干预
信息携带通常只能返回一个整数错误码可以抛出任意类型(字符串、对象等),信息丰富
强制处理调用者可能忘记检查返回值未捕获的异常会导致程序终止,不会"静默忽略"

🔥 高频考点: GESP 四级常以选择题形式考查"异常处理相比错误码的优势",核心记住三点:分离正常逻辑与错误处理、自动传播、信息丰富。


三、异常处理三件套:try / throw / catch ​

C++ 异常处理的核心由三个关键字组成:

关键字作用形象比喻
try包裹可能出错的代码块"试试看"——把可能出问题的代码放进来
throw检测到错误时抛出异常"抛出去"——发现问题,立刻报告
catch捕获并处理异常"接住"——拿到异常信息,进行处理

三者的配合流程:

try 块中代码正常执行
        │
        ├── 没有 throw → 跳过所有 catch,继续执行后续代码
        │
        └── 遇到 throw → 立即跳出 try,按类型匹配 catch
                              │
                              ├── 匹配成功 → 执行 catch 块 → 继续后续代码
                              └── 匹配失败且无 catch(...) → 调用 std::terminate(),程序终止

3.1 最简示例 ​

cpp
#include <iostream>
using namespace std;

int main() {
    try {
        int a = 10, b = 0;
        if (b == 0) {
            throw "除数不能为零!";   // 抛出 const char* 类型异常
        }
        cout << a / b << endl;       // 这行不会执行
    }
    catch (const char* msg) {
        // 捕获 const char* 类型异常
        cout << "发生异常:" << msg << endl;
    }

    cout << "程序继续运行..." << endl;  // catch 执行完后继续到这里
    return 0;
}

输出:

发生异常:除数不能为零!
程序继续运行...

⚠️ 易错点: try 和 catch 必须配对使用!有 try 就必须有至少一个 catch,否则编译报错。throw 可以出现在 try 块内,也可以出现在被 try 块调用的函数中。


四、抛出基本类型异常 ​

throw 后面可以跟任意类型的值,最基本的是抛出内置类型:

4.1 抛出字符串 ​

cpp
#include <iostream>
using namespace std;

int main() {
    try {
        throw "文件打开失败";  // 抛出 const char* 类型
    }
    catch (const char* msg) {
        cout << "错误:" << msg << endl;
    }
    return 0;
}

4.2 抛出整数 ​

cpp
#include <iostream>
using namespace std;

int main() {
    try {
        int errorCode = 404;
        throw errorCode;  // 抛出 int 类型
    }
    catch (int code) {
        cout << "错误码:" << code << endl;
    }
    return 0;
}

4.3 抛出浮点数 ​

cpp
#include <iostream>
using namespace std;

int main() {
    try {
        throw 3.14;  // 抛出 double 类型
    }
    catch (double val) {
        cout << "异常值:" << val << endl;
    }
    return 0;
}

🔥 高频考点: catch 的类型匹配是严格按类型的,不会做隐式算术类型转换。抛出 int 不会被 catch (double) 捕获!抛出 const char* 也不会被 catch (string) 捕获。如果类型不匹配且没有 catch(...) 兜底,程序会直接终止。


五、抛出对象异常 ​

除了基本类型,throw 还可以抛出类对象。C++ 标准库提供了 std::exception 及其派生类,所有标准异常都有 what() 方法返回错误描述:

cpp
#include <iostream>
#include <stdexcept>   // runtime_error, logic_error 等
using namespace std;

int main() {
    try {
        // 抛出 runtime_error 对象,构造参数为错误描述
        throw runtime_error("运行时错误:数组下标越界");
    }
    catch (const runtime_error& e) {
        // what() 返回 const char* 类型的错误信息
        cout << "捕获 runtime_error:" << e.what() << endl;
    }
    return 0;
}

输出:

捕获 runtime_error:运行时错误:数组下标越界

自定义异常类 ​

我们也可以继承 std::exception 来定义自己的异常类:

cpp
#include <iostream>
#include <exception>
#include <string>
using namespace std;

// 自定义异常类,继承 std::exception
class DivideByZeroException : public exception {
private:
    string msg;
public:
    DivideByZeroException() : msg("除数不能为零!") {}
    // 重写 what() 方法
    const char* what() const noexcept override {
        return msg.c_str();
    }
};

double divide(double a, double b) {
    if (b == 0) {
        throw DivideByZeroException();  // 抛出自定义异常对象
    }
    return a / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl;  // 输出 5
        cout << divide(10, 0) << endl;  // 抛出异常
    }
    catch (const DivideByZeroException& e) {
        cout << "错误:" << e.what() << endl;
    }
    return 0;
}

💡 了解即可: GESP 四级不要求自己写异常类,但需要知道 throw 可以抛出对象,以及 what() 方法的作用。自定义异常类的代码重在帮助理解"对象异常"的概念。


六、多个 catch 块的匹配规则 ​

当一个 try 块可能抛出多种类型的异常时,可以编写多个 catch 块,从上到下依次匹配,匹配到第一个类型相符的 catch 就执行,后面的不再检查:

cpp
#include <iostream>
using namespace std;

void process(int type) {
    if (type == 1) throw "字符串异常";   // const char*
    if (type == 2) throw 404;           // int
    if (type == 3) throw 3.14;          // double
}

int main() {
    for (int t = 1; t <= 3; t++) {
        try {
            process(t);
        }
        catch (const char* msg) {
            cout << "捕获字符串:" << msg << endl;
        }
        catch (int code) {
            cout << "捕获整数:" << code << endl;
        }
        catch (double val) {
            cout << "捕获浮点数:" << val << endl;
        }
    }
    return 0;
}

输出:

捕获字符串:字符串异常
捕获整数:404
捕获浮点数:3.14

继承关系下的匹配 ​

当异常类存在继承关系时,父类引用可以捕获子类对象。因此,catch 块的排列顺序应当先子类、后父类:

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

int main() {
    try {
        throw out_of_range("下标 10 超出范围 [0, 5)");
    }
    catch (const out_of_range& e) {
        // 先匹配子类(更具体)
        cout << "越界异常:" << e.what() << endl;
    }
    catch (const exception& e) {
        // 后匹配父类(更通用)
        cout << "标准异常:" << e.what() << endl;
    }
    return 0;
}

⚠️ 易错点: 如果把 catch (const exception& e) 放在 catch (const out_of_range& e) 前面,那么所有 exception 的派生类异常都会被父类 catch "截胡",子类 catch 永远无法执行。部分编译器会对此发出警告。正确的顺序是:从具体到通用,从子类到父类。


七、catch(...):捕获所有异常 ​

catch (...) 可以捕获任意类型的异常,通常放在所有 catch 块的最后,作为安全网:

cpp
#include <iostream>
using namespace std;

int main() {
    try {
        throw 'X';  // 抛出 char 类型
    }
    catch (int code) {
        cout << "整数异常:" << code << endl;
    }
    catch (...) {
        // 兜底:捕获所有未被前面 catch 匹配的异常
        cout << "捕获到未知类型的异常" << endl;
    }
    return 0;
}

输出:

捕获到未知类型的异常

🔥 高频考点: catch (...) 必须放在所有 catch 块的最后!如果放在前面,它会"截胡"所有异常,导致后面的具体类型 catch 永远无法到达。这是判断题和选择题的经典陷阱。


八、异常的传播机制 ​

异常不仅能在当前函数中被捕获,还能沿着函数调用栈向上传播。如果当前函数的 try-catch 没有匹配到异常,异常会自动传递给调用者,逐层向上,直到找到匹配的 catch 或到达 main 函数仍未被捕获:

cpp
#include <iostream>
using namespace std;

// 最内层函数:抛出异常,但不捕获
void funcC() {
    cout << "funcC 开始" << endl;
    throw "来自 funcC 的异常";  // 抛出异常
    cout << "funcC 结束" << endl;  // 不会执行
}

// 中间层函数:调用 funcC,也不捕获
void funcB() {
    cout << "funcB 开始" << endl;
    funcC();  // 异常传播到这里,继续向上
    cout << "funcB 结束" << endl;  // 不会执行
}

// 最外层函数:捕获异常
void funcA() {
    cout << "funcA 开始" << endl;
    try {
        funcB();  // 异常最终在这里被捕获
    }
    catch (const char* msg) {
        cout << "funcA 捕获:" << msg << endl;
    }
    cout << "funcA 结束" << endl;
}

int main() {
    funcA();
    cout << "main 继续执行" << endl;
    return 0;
}

输出:

funcA 开始
funcB 开始
funcC 开始
funcA 捕获:来自 funcC 的异常
funcA 结束
main 继续执行

传播过程示意 ​

main()
  └─ funcA()
       └─ try { funcB() }
            └─ funcB()
                 └─ funcC()
                      └─ throw "异常"   ← 异常在这里抛出
                           ↓ 向上传播
                      funcC 中无 catch → 继续
                           ↓
                 funcB 中无 catch → 继续
                           ↓
            funcA 的 try 有 catch(const char*) ← 匹配成功,处理!

🔥 高频考点: 异常传播时,从抛出点到 catch 之间的所有局部对象都会被析构(栈展开,Stack Unwinding)。GESP 四级不要求深入栈展开细节,但需要理解:异常会跳过调用链中未捕获它的函数,这些函数中 throw 之后的代码不会执行。这是程序阅读题的核心考点。


九、标准异常类层次 ​

C++ 标准库在 <stdexcept> 和 <exception> 头文件中定义了一组异常类,构成继承体系:

std::exception              // 基类,定义在 <exception>
├── std::logic_error        // 逻辑错误(理论上可通过代码检查避免)
│   ├── std::invalid_argument  // 无效参数
│   ├── std::out_of_range      // 越界(如 vector::at())
│   ├── std::length_error      // 长度超限
│   └── std::domain_error      // 定义域错误
├── std::runtime_error      // 运行时错误(难以提前预防)
│   ├── std::overflow_error    // 上溢
│   ├── std::underflow_error   // 下溢
│   └── std::range_error       // 范围错误
├── std::bad_alloc          // new 分配内存失败
└── std::bad_cast           // dynamic_cast 失败
异常类所属类别典型场景
std::exception基类所有标准异常的父类,定义 what()
std::logic_error逻辑错误程序逻辑有误,理论上可避免
std::runtime_error运行时错误运行时才能发现的问题
std::out_of_rangelogic_error 派生容器越界(如 vector::at())
std::invalid_argumentlogic_error 派生参数不合法
std::bad_alloc直接派生new 内存分配失败

所有标准异常类都继承自 std::exception,并重写了 what() 方法。因此,用一个 catch (const exception& e) 就可以捕获所有标准异常:

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

int main() {
    try {
        // 模拟不同的标准异常
        throw out_of_range("下标越界");
    }
    catch (const exception& e) {
        // 父类引用捕获所有派生类异常
        cout << "捕获标准异常:" << e.what() << endl;
    }
    return 0;
}

💡 了解即可: GESP 四级不要求记住所有标准异常类的名称,但需要知道:① std::exception 是基类;② what() 方法用于获取错误描述;③ 用 catch (const exception& e) 可以捕获所有标准异常。这三点在选择题中可能出现。


十、noexcept 说明符简介 ​

C++11 引入了 noexcept 关键字,用于声明一个函数不会抛出异常:

cpp
#include <iostream>
using namespace std;

// noexcept 告诉编译器:这个函数保证不抛出异常
int safeDivide(int a, int b) noexcept {
    // 注意:如果在 noexcept 函数中抛出了异常且未在函数内捕获,
    // 程序会直接调用 std::terminate() 终止,不会传播!
    if (b == 0) return 0;  // 不抛异常,返回默认值
    return a / b;
}

int main() {
    cout << safeDivide(10, 2) << endl;  // 输出 5
    cout << safeDivide(10, 0) << endl;  // 输出 0(不抛异常)
    return 0;
}
关键字含义
noexcept函数保证不抛出异常;若抛出则程序直接终止
throw()C++11 之前的写法,效果类似 noexcept,已不推荐使用

⚠️ 易错点: noexcept 不是"try 不 catch",而是对编译器的承诺——这个函数不会让异常传播出去。如果在 noexcept 函数中抛出了异常且未在函数内部捕获,程序会直接调用 std::terminate() 终止,不会进行栈展开。GESP 四级对 noexcept 仅要求了解概念,能在选择题中认出即可。


十一、异常处理的最佳实践与常见陷阱 ​

✅ 最佳实践 ​

  1. 按引用捕获异常对象:使用 catch (const exception& e) 而非 catch (exception e),避免对象切片和不必要的拷贝
  2. catch 块从具体到通用排列:先子类后父类,最后放 catch(...)
  3. 在合适的层级捕获:在能真正处理错误的地方捕获,而不是在每层都无脑 catch
  4. 捕获后要有恢复或退出逻辑:不能只打印一条消息就当什么都没发生

❌ 常见陷阱 ​

cpp
// 陷阱1:try 后没有 catch
try {
    throw 1;
}
// 编译错误:try 块必须至少跟一个 catch

// 陷阱2:catch(...) 放在最前面
try {
    throw 1;
}
catch (...) {
    cout << "兜底";
}
catch (int) {  // 永远无法到达!
    cout << "整数";
}

// 陷阱3:类型不匹配
try {
    throw "字符串";     // const char* 类型
}
catch (int code) {    // 不匹配!
    cout << code;
}
// 没有 catch(const char*) 也没有 catch(...)
// → 程序调用 std::terminate() 终止!

// 陷阱4:throw 后误以为代码会继续执行
try {
    cout << "A";
    throw 1;
    cout << "B";   // 永远不会输出!
}
catch (int) {
    cout << "C";
}
// 输出:AC

🔥 必记规则: throw 一旦执行,当前 try 块中紧随其后的所有代码都不会执行,程序直接跳转到类型匹配的 catch 块。这是 GESP 程序阅读题最高频的考点,几乎每场必考!


十二、竞赛中是否使用异常? ​

这是一个很多同学关心的实际问题。在 NOIP、CSP、GESP 等算法竞赛中:

场景是否推荐使用异常原因
算法竞赛编程题❌ 一般不推荐竞赛中的错误通常是逻辑 bug,而非"预期的异常";异常有性能开销,且 OI 赛制下程序崩溃可通过调试修复
GESP 选择题/判断题✅ 必须掌握语法考试会考查 try-catch-throw 的语法和执行流程,能读懂代码、判断输出
GESP 编程题⚠️ 按需使用如果题目明确要求异常处理则使用;否则用常规条件判断即可,不强制
实际工程开发✅ 推荐使用文件操作、网络通信、用户输入等场景,异常处理是标准做法

💡 简单总结: 竞赛编程中,异常处理更多是考查概念而非编程实践。考场上能读懂异常处理代码、判断输出结果即可;编程题中除非题目要求,否则用 if 判断边界条件更直接、更高效。


十三、📝 实战练习 ​

练习题 1(程序阅读题) ​

题目: 阅读以下程序,输出结果是( )

cpp
#include <iostream>
using namespace std;

int func(int a, int b) {
    if (b == 0) throw "除数为零";
    return a / b;
}

int main() {
    try {
        cout << func(10, 3) << " ";
        cout << func(10, 0) << " ";
        cout << func(10, 2) << " ";
    }
    catch (const char* msg) {
        cout << msg;
    }
    cout << "!" << endl;
    return 0;
}

A. 3 除数为零!
B. 3 除数为零 5!
C. 3 除数为零
D. 除数为零!

🔑 点击查看答案与解析

答案: A

解析:

步骤执行内容输出
1func(10, 3) 正常返回 3,输出 3 3
2func(10, 0) 抛出 "除数为零",立即跳出 try—
3func(10, 2) 不会执行(throw 后 try 中代码被跳过)—
4匹配到 catch(const char*),输出 除数为零除数为零
5catch 执行完毕,继续执行后续 cout << "!"!

完整输出:3 除数为零!,选 A。

⚠️ 本题的核心陷阱:func(10, 2) 不会执行!很多同学误以为异常被 catch 后会回到 try 继续执行,实际上 catch 处理完后直接跳到 try-catch 之后。


练习题 2(单选题) ​

题目: 以下程序的输出结果是( )

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

void test(int n) {
    if (n == 0) throw runtime_error("运行时错误");
    if (n == 1) throw 404;
    if (n == 2) throw "字符串错误";
}

int main() {
    for (int i = 0; i <= 2; i++) {
        try {
            test(i);
        }
        catch (int code) {
            cout << "整数:" << code << " ";
        }
        catch (const char* msg) {
            cout << "字符串:" << msg << " ";
        }
        catch (const exception& e) {
            cout << "异常:" << e.what() << " ";
        }
    }
    cout << endl;
    return 0;
}

A. 异常:运行时错误 整数:404 字符串:字符串错误 ※注意末尾没有空格
B. 整数:404 字符串:字符串错误 异常:运行时错误
C. 异常:运行时错误 整数:404 字符串:字符串错误 ※注意末尾有空格
D. 程序崩溃

🔑 点击查看答案与解析

答案: C

解析:

i抛出类型匹配的 catch输出
0runtime_error 对象catch (const exception& e)(runtime_error 是 exception 的派生类)异常:运行时错误
1404(int)catch (int code)整数:404
2"字符串错误"(const char*)catch (const char* msg)字符串:字符串错误

注意 i=0 时,runtime_error 先尝试匹配 catch (int)——不匹配;再尝试 catch (const char*)——不匹配;最后匹配 catch (const exception&)——成功(父类引用可捕获子类对象)。

三次循环输出后换行,完整输出为 异常:运行时错误 整数:404 字符串:字符串错误 (末尾有空格),选 C。


练习题 3(编程题) ​

题目: 以下程序实现了一个读取文件首行整数的函数。如果文件无法打开,抛出 string 类型异常;如果文件内容不是有效整数,抛出 const char* 类型异常。请补全代码中的空缺。

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

int readFirstInt(const string& filename) {
    ifstream fin(filename);
    if (______①______) {
        throw filename;              // 抛出 string 类型
    }
    int value;
    fin >> value;
    if (fin.fail()) {
        ______②______;               // 抛出 const char* 类型
    }
    fin.close();
    return value;
}

int main() {
    try {
        int n = readFirstInt("data.txt");
        cout << "读取到:" << n << endl;
    }
    catch (______③______) {
        cout << "文件错误:无法打开 " << name << endl;
    }
    catch (______④______) {
        cout << "内容错误:" << msg << endl;
    }
    return 0;
}
🔑 点击查看答案与解析

答案:

  • ① !fin.is_open()(或等价的 !fin)
  • ② throw "文件内容不是有效整数"
  • ③ const string& name
  • ④ const char* msg

解析:

  • ① 处判断文件是否成功打开:fin.is_open() 返回 true 表示成功,取反表示失败
  • ② 处需要抛出 const char* 类型异常,使用 throw 加字符串字面量即可
  • ③ 处捕获 ① 抛出的 filename(string 类型),使用 const string& name,变量名 name 与 catch 块中一致
  • ④ 处捕获 ② 抛出的 const char* 类型,使用 const char* msg,变量名 msg 与 catch 块中一致

🔥 考点: 异常抛出和捕获的类型必须严格一致。抛出 string 就必须用 const string& 或 string 捕获;抛出 const char* 就必须用 const char* 捕获。两者不能混淆!


练习题 4(判断题) ​

题目: 编译错误也是一种异常,可以用 try-catch 捕获。( )

🔑 点击查看答案与解析

答案: 错误 ❌

解析: 异常是运行时发生的非正常情况,而编译错误是在编译阶段就被编译器发现的语法或类型错误,程序根本无法生成可执行文件,自然也无法用 try-catch 捕获。

🍎 生活比喻:编译错误就像菜谱写错了(比如把"盐"写成"监"),厨师一看就知道不对,根本不会开始做;异常是菜谱没问题,但做菜过程中发现盐罐空了(运行时意外)。


练习题 5(单选题) ​

题目: 关于 catch(...) 的说法,正确的是( ) A. catch(...) 可以捕获所有类型的异常,应该放在 catch 块的最前面
B. catch(...) 只能捕获 int 类型的异常
C. catch(...) 是"兜底"catch,应该放在所有 catch 块的最后面
D. 一个 try 块只能有一个 catch(...) 和一个普通 catch

🔑 点击查看答案与解析

答案: C

解析:

  • A 错误:catch(...) 确实能捕获所有类型异常,但必须放在最后面,放前面的话后面的 catch 永远不会执行
  • B 错误:catch(...) 可以捕获任何类型的异常,不只是 int
  • C 正确 ✅:catch(...) 是"兜底"机制,前面的 catch 都匹配不到时才由它处理,所以必须放最后
  • D 错误:一个 try 可以跟多个 catch 块,再加一个 catch(...) 兜底

💡 记忆技巧:catch 块像安检通道——先查特定证件(具体类型),都不符合的走人工通道(catch(...))。人工通道放最前面的话,特定通道就没人走了。


练习题 6(程序阅读题) ​

题目: 阅读以下程序,输出结果是( )

cpp
#include <iostream>
using namespace std;

void inner() {
    throw "inner error";
}

void middle() {
    inner();
    cout << "M";  // 不会执行
}

int main() {
    try {
        middle();
        cout << "A";
    }
    catch (const char* msg) {
        cout << "C:" << msg;
    }
    cout << "E" << endl;
    return 0;
}

A. MC:Ainner errorE
B. C:inner errorE
C. AC:inner errorE
D. C:inner error

🔑 点击查看答案与解析

答案: B

解析:

执行流程:

  1. main 中调用 middle()
  2. middle 调用 inner()
  3. inner 抛出 const char* 类型异常
  4. inner 中没有 catch,异常向上传播到 middle
  5. middle 中也没有 catch,异常继续向上传播到 main
  6. main 中有 catch(const char*),匹配成功
  7. 输出 C:inner error
  8. catch 执行完毕,继续执行后面的 cout << "E"

最终输出:C:inner errorE,选 B。

🔥 关键结论:异常会沿调用栈向上传播,直到找到匹配的 catch。如果一直到 main 都没有 catch,程序就崩溃了(调用 terminate)。


练习题 7(单选题) ​

题目: 关于 C++ 标准异常类,以下说法错误的是( ) A. std::exception 是所有标准异常类的基类
B. 标准异常类都有 what() 方法,返回错误描述
C. catch (const exception& e) 可以捕获所有标准异常
D. 抛出 int 类型的异常可以被 catch (const exception& e) 捕获

🔑 点击查看答案与解析

答案: D

解析:

  • A 正确 ✅:std::exception 是标准异常的基类
  • B 正确 ✅:what() 方法返回 const char* 类型的错误描述
  • C 正确 ✅:父类引用可以捕获所有子类对象(面向对象的多态)
  • D 错误 ❌:int 不是 exception 的派生类,类型不匹配,无法被捕获

💡 理解技巧:标准异常是一个家族,exception 是族长。族长的接待室(catch exception&)可以接待所有家族成员,但外乡人(int、char* 等)不行。


练习题 8(编程题) ​

题目: 请编写一个函数 double sqrt_positive(double x),计算 x 的平方根。要求:

  • 如果 x >= 0,返回平方根
  • 如果 x < 0,抛出 string 类型的异常,内容为 "负数没有平方根"

在主函数中测试:输入一个数,输出平方根;如果捕获到异常,输出错误信息。

提示:使用 sqrt(x) 函数计算平方根,需要 #include <cmath>。

🔑 点击查看答案与解析

参考答案:

cpp
#include <iostream>
#include <cmath>
#include <string>
using namespace std;

double sqrt_positive(double x) {
    if (x < 0) {
        throw string("负数没有平方根");
    }
    return sqrt(x);
}

int main() {
    double num;
    cin >> num;
    try {
        double result = sqrt_positive(num);
        cout << "平方根:" << result << endl;
    }
    catch (const string& msg) {
        cout << "错误:" << msg << endl;
    }
    return 0;
}

解析:

这是异常处理的典型应用:

  1. 函数检测到非法输入(负数)时,不返回错误码,而是抛出异常
  2. 调用者用 try-catch 包裹,捕获异常并处理
  3. 异常类型为 string,catch 中用 const string& 捕获(引用避免拷贝)

💡 异常的优势:函数返回值只能返回一个值,用异常可以把"正常结果"和"错误信息"分开,代码更清晰。


十四、📊 考点总结表 ​

考点重要程度出题形式易错点
异常的概念(运行时错误)⭐⭐选择/判断和编译错误混淆
try-throw-catch 基本语法⭐⭐⭐⭐程序阅读/代码填空try 后必须有 catch
throw 后 try 代码不执行⭐⭐⭐⭐⭐程序阅读(必考)以为 catch 后会回到 try 继续
多个 catch 匹配顺序⭐⭐⭐⭐选择/程序阅读从具体到通用,父类放后面
catch(...) 兜底⭐⭐⭐选择/判断必须放最后,放前面会挡住后面的
异常沿调用栈传播⭐⭐⭐⭐程序阅读内层抛出的异常会向上传
标准异常类与 what()⭐⭐选择(了解)非标准异常不能被 exception& 捕获
异常类型不匹配⭐⭐⭐判断/程序阅读抛出和捕获类型必须一致
noexcept 说明符⭐选择(了解)抛出异常会直接 terminate
未捕获异常⭐⭐⭐判断程序调用 terminate 终止

⭐ 必背速记卡 ​

┌─────────────────────────────────────────────┐
│          异常处理 · 必背速记卡               │
├─────────────────────────────────────────────┤
│  1. 异常是运行时错误,编译错误不是异常       │
│  2. 异常三件套:try 保护 + throw 抛出 + catch捕获│
│  3. throw 后,try 中后续代码全部跳过        │
│  4. 异常沿调用栈向上传播,直到被捕获         │
│  5. 没被捕获的异常 → 程序 terminate(崩溃)  │
│  6. 多个 catch 按顺序匹配,从上到下          │
│  7. 先具体后通用,父类 catch 放后面          │
│  8. catch(...) 捕获所有,必须放最后面        │
│  9. 抛出和捕获类型必须匹配,不能张冠李戴     │
│ 10. std::exception 是标准异常基类           │
│ 11. 父类引用 catch 可以捕获子类异常对象      │
│ 12. what() 方法获取异常的文字描述            │
│ 13. noexcept 承诺不抛异常,抛了就直接终止    │
│ 14. try 块后面必须至少跟一个 catch          │
│ 15. catch 执行完后继续往下,不回 try 继续   │
└─────────────────────────────────────────────┘

百炼成钢,融会贯通