1、资料清单
资料
内容
入口
教材
《C++语言程序设计(第4版)》Chap12,原书页码 528-553
抽取 Markdown 下载
课件
Chap12 课件,位于本地郑莉 C++ 资料目录
本地资料夹
例题源代码
C++V5 源代码
下载
VS2019 工程
C++V5 VS2019 solution
下载
Chap12 的主线是异常发生 → 抛出异常 → 栈展开 → 捕获处理 → 资源自动释放 → 保持对象状态可靠 。异常处理不是单纯的语法点,它真正要解决的是“错误发生后,程序还能不能有秩序地收场”。
2、颜色标注
颜色
含义
使用位置
蓝色
核心术语、定义
异常、抛出、捕获、栈展开、标准异常
绿色
推荐写法、主线
按引用捕获、RAII、智能指针、标准异常类型
黄色
易错点、边界条件
catch 顺序、构造失败、析构函数抛异常
红色
不建议做法
裸指针跨异常路径、吞掉所有异常、析构函数主动抛出异常
3、学习目标
目标
要会到什么程度
异常思想
理解异常表示程序无法继续当前操作的特殊情况
语法机制
会使用 try、throw、catch
控制流
知道异常抛出后会跳过普通返回路径,进入匹配的处理器
标准异常
会使用 std::exception、std::runtime_error、std::out_of_range
构造析构
理解构造失败、析构释放和栈展开
资源管理
会用 RAII 与智能指针避免异常路径资源泄漏
异常安全
知道基本保证、强保证和不抛异常保证
4、章节目录
小节
内容
本节关键词
12.1
异常处理的基本思想
异常、错误恢复、分层处理
12.2
C++ 异常处理的实现
try、throw、catch、异常接口
12.3
异常处理中的构造与析构
构造失败、析构释放、栈展开
12.4
标准程序库异常处理
std::exception、bad_alloc、out_of_range
12.5
综合实例
银行账户程序改进
12.6
深度探索
异常安全、资源泄漏、RAII
12.7
小结
错误边界、资源边界、状态边界
5、异常处理的基本思想 异常 是程序执行中出现的特殊情况:当前函数无法继续完成自己的工作,需要把错误交给更合适的调用者处理。
传统错误处理常用返回值:
1 2 3 4 5 6 7 8 int divide (int a, int b, int & result) { if (b == 0 ) { return -1 ; } result = a / b; return 0 ; }
异常处理则把正常结果和错误路径分开:
1 2 3 4 5 6 7 8 9 #include <stdexcept> int divide (int a, int b) { if (b == 0 ) { throw std::invalid_argument ("除数不能为 0" ); } return a / b; }
两种方式不是绝对谁替代谁。一般来说:
情况
更适合
可以预期、频繁发生的普通分支
返回值、状态码
当前函数无法处理,需要向上传递
异常
构造函数失败
异常
资源申请失败
异常或 RAII 包装后的错误处理
6、try、throw、catch 异常处理的基本结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <stdexcept> int divide (int a, int b) { if (b == 0 ) { throw std::invalid_argument ("除数不能为 0" ); } return a / b; } int main () { try { std::cout << divide (10 , 0 ) << "\n" ; } catch (const std::invalid_argument& error) { std::cout << "参数错误:" << error.what () << "\n" ; } catch (const std::exception& error) { std::cout << "标准异常:" << error.what () << "\n" ; } return 0 ; }
关键字
作用
try
包住可能抛出异常的代码
throw
抛出一个异常对象
catch
捕获并处理匹配类型的异常
catch 的顺序要从具体到一般。比如先捕获 std::out_of_range,再捕获 std::exception。如果把基类写在前面,后面的派生类处理器就没有机会执行。
7、栈展开与资源释放 异常抛出后,程序会沿调用栈向上寻找匹配的 catch。在这个过程中,已经构造完成的局部对象会被自动析构,这个过程叫栈展开 。
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 #include <iostream> #include <stdexcept> class Trace {public : explicit Trace (const char * name) : name_(name) { std::cout << "构造 " << name_ << "\n" ; } ~Trace () { std::cout << "析构 " << name_ << "\n" ; } private : const char * name_; }; void work () { Trace local ("local" ) ; throw std::runtime_error ("work 失败" ); } int main () { try { work (); } catch (const std::exception& error) { std::cout << error.what () << "\n" ; } return 0 ; }
这也是 RAII 能和异常天然配合的原因:只要资源被对象管理,异常发生时析构函数就会负责清理。
析构函数中尽量不要抛异常。尤其在栈展开过程中,如果析构函数再次抛出异常,程序可能直接终止。
8、标准异常类 标准库异常类都以 std::exception 为基类,常用类型如下:
异常类型
典型场景
std::exception
标准异常基类
std::runtime_error
运行期错误
std::logic_error
逻辑错误基类
std::invalid_argument
参数无效
std::out_of_range
下标或范围越界
std::bad_alloc
动态内存分配失败
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <stdexcept> #include <vector> int main () { std::vector<int > values{1 , 2 , 3 }; try { std::cout << values.at (10 ) << "\n" ; } catch (const std::out_of_range& error) { std::cout << "越界:" << error.what () << "\n" ; } catch (const std::exception& error) { std::cout << "标准异常:" << error.what () << "\n" ; } return 0 ; }
values[10] 不做边界检查,values.at(10) 会抛出 std::out_of_range。学习阶段可以多用 at() 来暴露越界问题。
9、异常接口与 noexcept 教材里的“异常接口声明”属于较早期 C++ 语法背景。现代 C++ 已经不推荐动态异常说明,例如 void f() throw(int) 这类写法已经过时。
现在更常见的是:
写法
含义
void f()
不声明是否抛异常
void f() noexcept
承诺不抛异常
noexcept(expression)
根据表达式结果决定是否不抛异常
1 2 3 void releaseResource () noexcept { }
noexcept 常用于析构函数、移动构造、移动赋值和底层资源释放函数。
10、异常安全与 RAII 异常安全不是指“程序不会出错”,而是指异常发生后,程序仍然满足某种可靠性保证。
等级
含义
基本保证
异常发生后没有资源泄漏,对象仍处于有效状态
强保证
异常发生后程序状态不改变,像操作没有发生
不抛异常保证
操作承诺不会抛出异常
异常最容易暴露裸指针问题:
1 2 3 4 5 void bad () { int * p = new int (10 ); throw std::runtime_error ("中途失败" ); delete p; }
上面代码中 delete p 永远执行不到。更好的写法是使用智能指针:
1 2 3 4 5 6 7 #include <memory> #include <stdexcept> void good () { auto p = std::make_unique <int >(10 ); throw std::runtime_error ("中途失败" ); }
异常发生时,p 会自动析构,内存会自动释放。
11、课后习题答案 12-1 什么叫做异常?什么叫做异常处理? 异常是程序执行过程中出现的非正常情况,例如内存分配失败、参数越界、文件打不开、运算无法定义等。当前函数如果无法继续完成自己的任务,可以通过 throw 抛出异常对象,把错误交给上层调用者处理。
异常处理就是在设计程序时预先考虑这些意外情况,用 try 包住可能出错的代码,用 catch 捕获并处理异常,使程序在错误发生后仍然能有秩序地处理问题。
12-2 C++ 的异常处理机制有何优点? C++ 异常处理机制的主要优点是:异常的发现和异常的处理可以分离。
优点
说明
分离正常逻辑和错误逻辑
正常代码不必被大量错误返回值打断
跨函数传递错误
底层函数可以抛出异常,上层选择合适位置处理
支持类型区分
不同异常类型可以进入不同 catch
配合析构自动清理
栈展开会调用局部对象析构函数,便于 RAII
12-3 举例说明 throw、try、catch 的用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <stdexcept> double reciprocal (double value) { if (value == 0.0 ) { throw std::invalid_argument ("不能对 0 求倒数" ); } return 1.0 / value; } int main () { try { std::cout << reciprocal (0.0 ) << "\n" ; } catch (const std::invalid_argument& error) { std::cout << "参数错误:" << error.what () << "\n" ; } catch (const std::exception& error) { std::cout << "其他标准异常:" << error.what () << "\n" ; } return 0 ; }
执行流程:
try 中调用 reciprocal(0.0)。
函数发现参数为 0,执行 throw。
普通执行流程中断。
程序寻找匹配的 catch。
std::invalid_argument 被捕获并输出错误信息。
12-4 设计异常抽象类,并派生 OutOfMemory 和 RangeError 原题要求定义一个抽象异常类 Exception,再派生两个异常类。下面保留这个设计,同时使用现代 C++ 的引用捕获和智能指针来避免异常路径中的资源泄漏。
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 #include <iostream> #include <memory> #include <new> #include <string> class Exception {public : virtual ~Exception () = default ; virtual const char * what () const noexcept = 0 ; }; class OutOfMemory : public Exception {public : const char * what () const noexcept override { return "Out of memory!" ; } }; class RangeError : public Exception {public : explicit RangeError (long number) : message_("Number out of range. You used " + std::to_string(number) + "!" ) { } const char * what () const noexcept override { return message_.c_str (); } private : std::string message_; }; std::unique_ptr<unsigned int > allocateNumber () { try { return std::make_unique <unsigned int >(0 ); } catch (const std::bad_alloc&) { throw OutOfMemory (); } } void readNumber (unsigned int & value) { long number = 0 ; std::cout << "Enter an integer(0~1000): " ; std::cin >> number; if (number < 0 || number > 1000 ) { throw RangeError (number); } value = static_cast <unsigned int >(number); } void run () { auto number = allocateNumber (); readNumber (*number); std::cout << "The number is: " << *number << "\n" ; } int main () { try { run (); } catch (const Exception& error) { std::cout << error.what () << "\n" ; } return 0 ; }
输入 56:
1 2 Enter an integer(0~1000): 56 The number is: 56
输入 2000:
1 2 Enter an integer(0~1000): 2000 Number out of range. You used 2000!
这里用 std::unique_ptr 管理动态内存。即使 readNumber 抛出 RangeError,已经申请的内存也会自动释放。
12-5 使用 try、catch 处理 new 分配内存失败 标准 C++ 中,普通 new 分配失败时默认抛出 std::bad_alloc,不是返回空指针。因此现代写法应该捕获 std::bad_alloc。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <new> int main () { try { char * buffer = new char [512 ]; std::cout << "内存分配成功\n" ; delete [] buffer; } catch (const std::bad_alloc& error) { std::cout << "有异常产生:内存分配失败," << error.what () << "\n" ; } return 0 ; }
如果想练习原题中的“手动抛出字符型异常”,可以使用 std::nothrow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <new> int main () { char * buffer = nullptr ; try { buffer = new (std::nothrow) char [512 ]; if (buffer == nullptr ) { throw "内存分配失败!" ; } std::cout << "内存分配成功\n" ; } catch (const char * message) { std::cout << "有异常产生:" << message << "\n" ; } delete [] buffer; return 0 ; }
更推荐的实际写法是:
1 2 3 #include <memory> auto buffer = std::make_unique <char []>(512 );
这样异常发生时不需要手动 delete[]。
12-6 定义 CException,观察异常触发和捕获流程 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 #include <iostream> class CException {public : const char * reason () const { return "CException 类中的异常。" ; } }; void fn1 () { std::cout << "在子函数中触发 CException 类异常\n" ; throw CException (); } int main () { std::cout << "进入主函数\n" ; try { std::cout << "在 try 模块中,调用子函数\n" ; fn1 (); std::cout << "这行不会执行\n" ; } catch (const CException& error) { std::cout << "在 catch 模块中,捕获到 CException 类型异常:" ; std::cout << error.reason () << "\n" ; } catch (const char * message) { std::cout << "捕获到其他类型异常:" << message << "\n" ; } std::cout << "回到主函数,异常已被处理\n" ; return 0 ; }
运行输出:
1 2 3 4 5 进入主函数 在 try 模块中,调用子函数 在子函数中触发 CException 类异常 在 catch 模块中,捕获到 CException 类型异常:CException 类中的异常。 回到主函数,异常已被处理
注意 fn1() 抛出异常后,try 中 fn1() 后面的普通语句不会继续执行,而是直接跳到匹配的 catch。
12、本章复盘清单
检查项
是否掌握
能解释异常和异常处理的含义
□
能写出 try、throw、catch 的基本结构
□
能按引用捕获异常,避免对象切片
□
能说明 catch 顺序为什么要从具体到一般
□
能说清栈展开和局部对象析构的关系
□
能使用 std::exception 及常见标准异常类
□
能说明普通 new 失败会抛出 std::bad_alloc
□
能用 RAII 或智能指针避免异常路径资源泄漏
□
能理解析构函数不应主动抛出异常
□
Chap12 是 C++ 基础课的收束点:异常处理把函数调用、对象生命周期、动态内存、文件流和标准库连接到一起。以后写项目时,不要只问“代码能不能跑”,还要问“出错时资源会不会释放,状态会不会乱掉”。