Appearance
第9章 异常处理
🎯 本章学习目标
通过本章的学习,你将能够:
- 理解异常概念:知道异常是程序运行时的非正常情况,区别于编译错误
- 掌握try-throw-catch:熟练使用异常处理三件套的基本语法
- 理解执行流程:清楚 throw 后 try 中代码被跳过,直接跳转到匹配的 catch
- 掌握多catch匹配:理解多个 catch 块按顺序匹配的规则
- 了解异常传播:知道异常会沿调用栈向上传播,直到被捕获
- 使用catch(...):知道"..."可以捕获所有类型的异常,但必须放最后
- 了解标准异常:知道 std::exception 是基类,what() 方法获取描述
- 区分异常类型:能判断抛出和捕获的类型是否匹配
- 理解noexcept:知道 noexcept 声明函数不抛出异常的含义
- 阅读异常代码:能快速分析异常处理代码的输出结果
🗺️ 本章知识图谱
异常处理
├── 基础概念
│ ├── 异常的定义(运行时非正常情况)
│ ├── 编译错误 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_range | logic_error 派生 | 容器越界(如 vector::at()) |
std::invalid_argument | logic_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仅要求了解概念,能在选择题中认出即可。
十一、异常处理的最佳实践与常见陷阱
✅ 最佳实践
- 按引用捕获异常对象:使用
catch (const exception& e)而非catch (exception e),避免对象切片和不必要的拷贝 - catch 块从具体到通用排列:先子类后父类,最后放
catch(...) - 在合适的层级捕获:在能真正处理错误的地方捕获,而不是在每层都无脑 catch
- 捕获后要有恢复或退出逻辑:不能只打印一条消息就当什么都没发生
❌ 常见陷阱
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
解析:
| 步骤 | 执行内容 | 输出 |
|---|---|---|
| 1 | func(10, 3) 正常返回 3,输出 3 | 3 |
| 2 | func(10, 0) 抛出 "除数为零",立即跳出 try | — |
| 3 | func(10, 2) 不会执行(throw 后 try 中代码被跳过) | — |
| 4 | 匹配到 catch(const char*),输出 除数为零 | 除数为零 |
| 5 | catch 执行完毕,继续执行后续 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 | 输出 |
|---|---|---|---|
| 0 | runtime_error 对象 | catch (const exception& e)(runtime_error 是 exception 的派生类) | 异常:运行时错误 |
| 1 | 404(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
解析:
执行流程:
main中调用middle()middle调用inner()inner抛出const char*类型异常inner中没有 catch,异常向上传播到middlemiddle中也没有 catch,异常继续向上传播到mainmain中有catch(const char*),匹配成功- 输出
C:inner error - 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;
}解析:
这是异常处理的典型应用:
- 函数检测到非法输入(负数)时,不返回错误码,而是抛出异常
- 调用者用 try-catch 包裹,捕获异常并处理
- 异常类型为
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 继续 │
└─────────────────────────────────────────────┘