1、资料清单

资料 内容 入口
教材 《C++语言程序设计(第4版)》Chap12,原书页码 528-553 抽取 Markdown 下载
课件 Chap12 课件,位于本地郑莉 C++ 资料目录 本地资料夹
例题源代码 C++V5 源代码 下载
VS2019 工程 C++V5 VS2019 solution 下载

Chap12 的主线是异常发生 → 抛出异常 → 栈展开 → 捕获处理 → 资源自动释放 → 保持对象状态可靠。异常处理不是单纯的语法点,它真正要解决的是“错误发生后,程序还能不能有秩序地收场”。

2、颜色标注

颜色 含义 使用位置
蓝色 核心术语、定义 异常、抛出、捕获、栈展开、标准异常
绿色 推荐写法、主线 按引用捕获、RAII、智能指针、标准异常类型
黄色 易错点、边界条件 catch 顺序、构造失败、析构函数抛异常
红色 不建议做法 裸指针跨异常路径、吞掉所有异常、析构函数主动抛出异常

3、学习目标

目标 要会到什么程度
异常思想 理解异常表示程序无法继续当前操作的特殊情况
语法机制 会使用 trythrowcatch
控制流 知道异常抛出后会跳过普通返回路径,进入匹配的处理器
标准异常 会使用 std::exceptionstd::runtime_errorstd::out_of_range
构造析构 理解构造失败、析构释放和栈展开
资源管理 会用 RAII 与智能指针避免异常路径资源泄漏
异常安全 知道基本保证、强保证和不抛异常保证

4、章节目录

小节 内容 本节关键词
12.1 异常处理的基本思想 异常、错误恢复、分层处理
12.2 C++ 异常处理的实现 trythrowcatch、异常接口
12.3 异常处理中的构造与析构 构造失败、析构释放、栈展开
12.4 标准程序库异常处理 std::exceptionbad_allocout_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、trythrowcatch

异常处理的基本结构:

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 举例说明 throwtrycatch 的用法

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;
}

执行流程:

  1. try 中调用 reciprocal(0.0)
  2. 函数发现参数为 0,执行 throw
  3. 普通执行流程中断。
  4. 程序寻找匹配的 catch
  5. std::invalid_argument 被捕获并输出错误信息。

12-4 设计异常抽象类,并派生 OutOfMemoryRangeError

原题要求定义一个抽象异常类 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 使用 trycatch 处理 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() 抛出异常后,tryfn1() 后面的普通语句不会继续执行,而是直接跳到匹配的 catch

12、本章复盘清单

检查项 是否掌握
能解释异常和异常处理的含义
能写出 trythrowcatch 的基本结构
能按引用捕获异常,避免对象切片
能说明 catch 顺序为什么要从具体到一般
能说清栈展开和局部对象析构的关系
能使用 std::exception 及常见标准异常类
能说明普通 new 失败会抛出 std::bad_alloc
能用 RAII 或智能指针避免异常路径资源泄漏
能理解析构函数不应主动抛出异常

Chap12 是 C++ 基础课的收束点:异常处理把函数调用、对象生命周期、动态内存、文件流和标准库连接到一起。以后写项目时,不要只问“代码能不能跑”,还要问“出错时资源会不会释放,状态会不会乱掉”。