Chapter 13 Exceptions¶
1 Why Exceptions¶
C++ 的理念是格式错误的代码不应运行,但运行时仍可能出现各种异常情况,程序必须设计好错误处理机制。
以读文件为例,若用错误码处理,每一步都要手动嵌套判断,结果是正常逻辑被大量错误处理代码淹没。
错误码风格
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if ( theFilesOpen ) {
determine its size;
if ( gotTheFileLength ) {
allocate that much memory;
if ( gotEnoughMemory ) {
read the file into memory;
if ( readFailed ) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if ( theFILEDidntClose && errorCode == 0 ) {
errorCode = -4;
}
} else {
errorCode = -5;
}
return errorCode;
}
异常(Exceptions)的价值在于分离了正常业务逻辑和错误处理逻辑。
异常风格
try {
open_the_file();
determine_size();
allocate_memory();
read_file();
close_file();
} catch (fileOpenFailed) {
doSomething();
} catch (sizeDeterminationFailed) {
doSomething();
} catch (memoryAllocationFailed) {
doSomething();
} catch (readFailed) {
doSomething();
} catch (fileCloseFailed) {
doSomething();
}
2 Exceptions Throwing & Propagation¶
Vector Index Error
template <class T>
class Vector {
private:
T* m_elements;
int m_size;
public:
Vector(int size = 0) : m_size(size) { /* ... */ }
~Vector() { delete[] m_elements; }
void length(int);
int length() { return m_size; }
T& operator[](int);
};
operator[] 越界怎么办?
- 返回随机内存:显然不行
- 返回魔法错误值:污染正常语义
- 直接
exit():库代码替调用方做决定 assert():只适合调试阶段- 更合理的方式:抛出异常,让调用者决定
抛出异常用 throw 关键字,典型做法是定义一个异常类,携带错误数据。
示例
class VectorIndexError {
public:
VectorIndexError(int v) : m_badValue(v) {}
void diagnostic() {
cerr << "index " << m_badValue << " out of range!";
}
private:
int m_badValue;
};
template <class T>
T& Vector<T>::operator[](int idx) {
if (idx < 0 || idx >= m_size) {
throw VectorIndexError(idx);
}
return m_elements[idx];
}
调用者的 4 种态度
int func() {
Vector<int> v(12);
v[3] = 5;
int i = v[42]; // out of range
// control never gets here!
return i * 5;
}
void outer() {
try {
func();
func2();
} catch (VectorIndexError& e) {
e.diagnostic();
// This exception does not propagate
}
cout << "Control is here after exception";
}
void outer2() {
String err("exception caught");
try {
func();
} catch (VectorIndexError) {
cout << err;
throw; // propagate the exception
}
}
void outer3() {
try {
outer2();
} catch (...) {
// ... catches ALL exceptions!
cout << "The exception stops here!";
}
}

控制流沿调用链向外回退,直到找到第一个匹配的处理器,栈上对象会在回退过程中正常析构(stack unwinding)。
What Happens During Propagation?
operator[]()抛出异常后,不会“返回错误码”,而是直接中断当前正常流程- 若
func()没有 handler,控制继续回退到outer2() outer2()若只是记录信息后执行throw;,异常会继续传播到outer3()outer3()中的catch (...)可以把这次传播真正截住
throw exp;:抛出一个新异常对象throw;:重新抛出当前正在处理的异常,仅在异常处理器内部有效
3 Exception Hierarchy¶
Try Block
try {
// ...
} catch (...) {
// ...
}
一个 try 后面可跟多个 catch,如果根本不打算处理,也可以不写 try,建立 handler 有运行时成本。
handler 选择时,先找精确类型匹配,再考虑基类转换(仅引用/指针),最后考虑 catch (...)。因此,使用 handler 时捕获顺序很重要,派生类 handler 应放在基类 handler 前面。
示例:using inheritance
class MathErr {
public:
virtual void diagnostic();
};
class OverflowErr : public MathErr {};
class UnderflowErr : public MathErr {};
class ZeroDivideErr: public MathErr {};
try {
// code to exercise math options
throw UnderFlowErr();
} catch (ZeroDivideErr& e) {
// handle zero divide case
} catch (MathErr& e) {
// handle other math errors
} catch (UnderFlowErr& e) {
// handle underflow errors
} catch (...) {
// any other exceptions
}

new:分配失败时不会返回0,而是抛出std::bad_alloc。
示例
try {
while (1) {
char* p = new char[10000];
}
} catch (std::bad_alloc& e) {
// ...
}
- 异常说明(Exception specifications):用于指定函数是否可能抛出异常,是函数类型的一部分,但不属于函数签名。
示例:noexcept
void abc(int a) noexcept {
// ...
}
编译器不一定会在编译期检查它,但会利用它来进行特定优化。如果该函数抛出了异常,程序会调用 std::terminate 直接终止运行。
4 Design Considerations¶
异常应用于表示错误,不应用于替代正常流程控制。
不合适的用法
try {
for (;;) {
p = list.next();
}
} catch (List::end_of_list) {
// 用异常表示普通迭代结束,不推荐
}
不能用异常代替良好设计,资源释放应交给对象析构(RAII),不要手工写伪 finally 式清理。
更好的资源管理
void func() {
File f("some file");
// 假设析构函数自动关闭文件
if (f.ok()) {
// ...
}
}
5 Constructors, Destructors and Exceptions¶
构造函数不能返回错误码,更好的方式是直接抛异常。
如果构造函数抛异常,该对象自身的析构函数不会被调用。若在抛出前已手动申请资源,必须先清理,否则会泄漏。
两阶段构造(Two-stage construction)
- 第一阶段:构造函数只做基础初始化
- 初始化成员对象
- 初始化内置类型成员
- 把指针设为
0/nullptr
- 第二阶段:把额外可能失败的工作放进
Init()- 打开文件
- 网络连接
- 动态申请资源
Smart Pointers
智能指针在析构时会自动释放所管理的原生指针。
std::unique_ptrstd::shared_ptr
析构函数会在正常离开作用域或异常传播导致的栈展开时会被调用。其应遵循的规则是不能让异常从析构函数逃出去,如果析构函数在异常展开期间再次抛异常,程序会 std::terminate()。
6 Catch by Reference¶
为什么最好按引用捕获?
- 按值捕获会切片(slicing)
- 按指针捕获则把释放责任带进异常处理代码,容易泄漏
切片问题
struct X {};
struct Y : public X {};
try {
throw Y();
} catch (X x) {
// 已切片,分不清原来是 X 还是 Y
}
推荐方式
struct B {
virtual void print() { /* ... */ }
};
struct D : public B { /* ... */ };
try {
throw D();
} catch (B& b) {
b.print();
}
若异常最终无人捕获,会调用 std::terminate()。
示例
void my_terminate() {
/* ... */
}
set_terminate(my_terminate);
7 Exception-safe Code¶
代码在异常发生后仍应保持对象状态一致。
错误顺序
void withdrawMoney(int amount) {
reduceBalance(amount);
prepareCash(); // 这里抛异常
releaseCash();
}
- 如果
prepareCash()抛异常,余额已经被减少,系统状态不一致。 - 设计时应优先保证不变量不被破坏、资源有对象托管、失败后系统仍处于合理状态。
总原则
- 早期就制定错误处理策略
- 不要滥用
try/catch,应通过对象来管理资源的获取与释放 - 本地控制结构(如
if/else)能处理的,不要强行上异常 - 不是每个函数都能处理每种错误
- 为核心接口添加异常说明(如
noexcept) - 库代码不应擅自终止程序,应抛给调用者决定