10.12 异常处理¶
简介¶
Tips:异常是处理构造函数失败的唯一途径,虽然可以用“简单工厂模式”或者
Init()方法代替异常,但是前者要求在堆栈分配内存,后者又会导致刚创建的实例处于“无效”状态。
典型的异常包括失去数据库连接以及遇到意外输入等,异常处理机制为程序中异常检测和异常处理两部分的协作提供支持:
thorw表达式:throw表达式用于表示它遇到了无法处理的问题,我们说throw引发了异常
try语句块:异常处理部分使用try语句块处理异常,它以关键字try开始,并以一个或多个catch子句结束
异常类:用于在throw表达式和catch子句之间传递异常的具体信息
throw表达式¶
C++语言中我们通过抛出(throwing)一条表达式来引发(raised)一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码(handler)将用来处理该异常:
#include <stdexcept>
throw runtime_error("tomocat");
Tips:当执行一个
throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的catch模块,该catch可能是同一个函数中的局部catch,也可能位于直接或间接调用了发生异常的函数的另一个函数。一个异常如果没有被捕获,则它将调用标准库函数terminate终止当前的程序。
当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。如果对抛出异常的调用语句位于一个try语句内,则检查与该try块关联的catch子句。如果找到了匹配的catch就用该catch处理异常。否则,如果该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。
上述过程被称为栈展开过程,栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句为止,或者也可能一直没找到匹配的catch从而退出主函数。如果找到了一个匹配的catch子句,则程序进入该子句并执行其中的代码。
try语句块与catch子句¶
try语句块的语法是:
Tips:try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。
try {
program-statements
} catch (exception-declaration) {
handler-statements
} catch (exception-declaration) {
handler-statements
}
例子:
while (cin >> item1 >> item2) {
try {
// 执行item1和item2的操作, 失败了抛出runtime_error异常
} catch (runtime_error err) {
cout << err.what() << endl;
}
}
// catch(...)捕获所有异常, 即可以与任何类型的异常匹配
void foo() {
try {
// 这里的操作将引发并抛出一个异常
} catch(...) {
// 处理异常的某些特殊操作, 然后再抛出一个异常终止程序
throw;
}
}
如果一个程序没有try语句块且发生了异常,系统会调用terminate函数并终止当前程序的执行。当异常被抛出时,首先搜索该异常的函数,如果没能找到匹配的catch子句,那么终止该函数并在调用该函数的函数中继续寻找。如果还是没找到匹配的catch子句,这个新的函数也被终止,继续搜索调用它的函数。如果最终还是没能找到任何匹配的catch子句,系统会调用terminate函数并终止当前程序的执行。
构造函数与异常¶
构造函数再在进入其函数体之前首先执行初始值列表,由于在初始值列表抛出异常时构造函数体内的try语句块还未生效,所以构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常。要想处理构造函数初始值抛出的异常,我们必须将构造函数写成函数try语句块的形式。函数try语句块使得一组catch语句既能处理构造函数体(或析构函数体),也能处理构造函数的初始化过程(或析构函数体)。
template <typename T>
Foo<T>::Foo(std::initializer_list<T> il) try : data(std::make_shared<std::vector<T>>(il)) {
// 空函数体
} catch (const std::bad_alloc &e) { handle_out_of_memory(e); }
Tips:还有一种情况值得注意,在初始化构造函数的参数时也可能发生异常,这样的异常不属于函数
try语句块的一部分。函数try语句块只能处理构造函数开始后发生的异常,和其他函数调用一样,如果在参数初始化的过程发生了异常,则该异常属于调用表达式的一部分,并将在调用者所在的上下文中处理。
析构函数与异常¶
如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块中,并在析构函数内部得到处理。在实际的编程过程中,因为析构函数仅仅是释放资源,所以它不太可能抛出异常,所以标准库类型都能确保它们的析构函数不会发生异常。
标准异常¶
1. C++标准库异常¶
C++标准库定义了一组类用于报告标准库遇到的问题,它们分别定义在4个头文件中:
exception头文件中定义了最通用的异常类exception,它只报告异常的发生,不提供任何额外信息
stdexcept头文件中定义了几种常用的异常类,后续会列举
new头文件中定义了
bad_alloc异常类型type_info头文件中定义了bad_cast异常类型
2. stdexcept定义的异常¶
stdexcept头文件中定义的异常类如下:
| 异常类 | 含义 |
|---|---|
| exception | 最常见的问题 |
| runtime_error | 只有在运行时才能检测出的问题 |
| range_error | 运行时错误:生成的结果超出了有意义的值域范围 |
| overflow_error | 运行时错误:计算上溢 |
| underflow_error | 运行时错误:计算下溢 |
| logic_error | 程序逻辑错误 |
| domian_error | 逻辑错误:参数对应的结果值不存在 |
| invalid_argument | 逻辑错误:无效参数 |
| length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
| out_of_range | 逻辑错误:使用一个超出有效范围的值 |
3. 注意事项¶
我们只能以默认初始化的方式初始化exception、
bad_alloc和bad_cast对于除exception、
bad_alloc和bad_cast的异常类,我们应该用string对象或者C风格字符串初始化这些类型的对象,不允许使用默认初始化的方式异常类只定义了一个名为what的成员函数,返回一个提供错误信息的C风格字符串
如果异常类型有一个字符串初始值,那么what方法返回该字符串;对于其他无初始值的异常类型来说,what返回的内容由编译器决定
异常类层次¶
1. 标准库异常类¶
标准库异常类构造了如下继承体系:
exception
├── bad_cast
├── runtime_error
| ├── overflow_error
| ├── underflow_error
| ├── range_error
├── logic_error
| ├── domain_error
| ├── invalid_argument
| ├── out_of_range
| ├── length_error
├── bad_alloc
其中类型exception仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what的虚成员函数。其中what函数返回一个const char*并确保不会抛出任何异常。类exception、bac_cast和bad_alloc定义了默认构造函数,类runtime_error和logic_error没有默认构造函数,但是有一个可以接受C风格字符串或者标准库string类型实参的构造函数。
Tips:由于
what是虚函数,因此当我们捕获基类的引用时,对what函数的调用将执行与异常对象动态类型对应的版本。
2. 用户自定义异常类¶
我们也可以使用自己的异常类,需要继承自标准异常类:
// 用户自设定的异常类
class out_of_stock : public std::runtime_error {
public:
explicit out_of_stock(const std::string &s) : std::runtime_error(s) { }
};
异常安全¶
Effective C++:Strive for exception-safe code.
异常安全函数(Exception-safe function)即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型和不抛出异常型。
“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
“异常安全”由两个条件,当异常被抛出时,带有异常安全性的函数满足:
不泄露任何资源
不允许数据败坏
异常安全函数(Exception-safe function)提供以下三种保证之一:
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。抛出异常时对象可以保持缺省状态也可以保持调用前状态,客户端可以调用某个成员函数获得具体的状态。
强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功就是完全成功;如果函数失败程序会回复到“调用函数之前”的状态。(与此对比的是,如果调用一个只提供“基本承诺”的函数而真的出现异常,程序有可能处于任何状态——只要它是个合法状态)
不抛出异常(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如int、指针等等)身上的所有操作都提供nothrow保证。