欢乐C++ —— 3. 异常处理与断言
try-catch
异常是指在运行时的反常行为,其行为超出函数正常功能范围。当程序的某个模块检测到它无法处理的问题时,就需要用到异常处理。异常处理机制分为异常检测,异常处理两部分。
在C++ 中,异常处理包括:
- throw 表达式,异常检测使用throw 表达式来表明它遇到了无法处理的问题。
- try catch语句块,异常处理使用try语句块来捕获可能发生的异常。try 语句块包含 catch 子句,try 语句块中的代码抛出的异常通常被某个catch 子句处理。
- 异常类,用于在throw 和 catch 之间传递异常信息。
简单示例
#include <stdexcept>
#include <iostream>
using namespace std;
int main( ) {
int n = 60;
cout << "before try catch" << endl;
try {
if ( n != 10 ) {
throw runtime_error( "n != 10 !!");
}
if ( n == 60 ) {
throw runtime_error("n == 60 !!");
}
}
catch (runtime_error err) {
std::cout << err.what( ) << endl;
}
cout << "after try catch" << endl;
return 0;
}
/*输出结果:
before try catch
n != 10 !!
after try catch
*/
常见的标准库异常
![image-20200409092638400](https://img2020.cnblogs.com/blog/1512048/202006/1512048-20200618115643478-1967528377.png)
进阶
当 throw 抛出的异常,则接下来 throw 后面的语句不会执行,而是寻找匹配的catch 子句。
按函数调用顺序一层一层查找匹配的catch 子句(这个过程称为 栈展开),每退出一个函数,销毁局部变量。如果直到主函数依然没有碰到catch 子句,则会调用terminate 终止当前程序。
意味着在栈展开的过程中,对象会自动调用析构函数,如果析构函数内又有可能抛出异常的话,那么这个异常应该在析构函数得到处理。
异常对象
异常对象是一种特殊对象,编译器使用异常抛出的 表达式 来对 异常对象 拷贝初始化。意味着当异常对象为类类型,则该类必须支持拷贝构造或移动构造。
异常对象处于编译器管理的空间中,编译器确保无论最终调用哪个catch 子句,其都能访问该异常对象。也就是说,catch 子句可以用引用捕获异常对象。
当异常处理完后,该异常对象被销毁。
try {
int n = 666;
cout << hex << &n<<endl;
throw n;
}
catch ( int &n ) {
cout << hex << &n;
}
/* 两个n 的地址不同,说明通过throw 的表达式拷贝了一个异常对象,而catch 引用了这个异常对象*/
抛出异常 throw
当抛出一条表达式时,表达式的静态类型就是异常对象的类型,这块没有动态绑定相关概念。
注意栈展开的过程局部变量会销毁,所以不要抛出局部变量的地址。
重新抛出 有时候一个单独的catch 语句不能完整的处理某个异常。在执行了某些校正操作之后当前的异常可能会接着让上一层的函数接着处理该异常,这时候可以通过重新抛出,将当前的异常对象传递给上一层调用链。 throw;
throw 可以抛出类对象,意味着我们可以自定义异常类。
捕获异常 catch
如果catch 不需要访问抛出的表达式,则直接可以省略形参名。
catch 是按照顺序逐一匹配。越是详细的异常就应该放在前面位置。意味着当具有继承关系,那么应该将派生类异常的处理代码放在基类异常的处理代码之前。
当进入一个catch 语句后,通过异常对象初始化异常声明中的参数。和函数的形参类似有引用和非引用之分,但不能是右值引用(因为在这个异常对象未处理完之前,它一直是左值)。
在实参形参传递的形式上和函数传参类似。不过异常对象的静态类型决定异常可以处理的类型,意味着即使是基类引用绑定到派生类对象,其异常类型依然为基类类型。
异常的匹配规则比函数形参实参匹配限制更多。
捕获所有异常 catch(…){}
异常与构造函数
想要捕获构造函数初始化列表中的异常就需要 函数try 语句块(function try block)。
class Test {
public :
int n;
Test(int x)try :n(x) { cout << "test" << endl; }
catch ( ... ) { }
};
noexcept
noexecpt 有两种语义:
-
异常声明符
声明某个函数不会抛出异常。对于用户来说,可以尽快排除异常情况;对于编译器来说,可以执行一些优化。
如果在noexcept 声明的函数内抛出异常,编译器不会报错,但在运行时会调用abort 终止进程。所以使用noexcept 的场景有两个:
- 声明某个函数不会抛出异常。
- 当我们不知道如何处理这个异常。(存疑,虽然C++primer 上这样写,但一般情况下都是要对所有异常情况进行处理)
-
运算符
检测函数是否可能抛出异常,返回bool 值
assert
assert 宏在
其它一些在调试中也很有用的宏:
-
_func_ 当前调试的函数的名字。
-
_FILE_ 存放文件名的字符串字面值。
-
_LINE_ 存放当前行号的整型字面值。
-
_TIME_ 存放文件编译时间的字符串字面值。
-
_DATE_ 存放文件编译日期的字符串字面值。
assert处理的是程序从逻辑上绝对不会出现问题的地方,只在调试版本有作用;而if 判断和try-catch 更像是对特殊条件的处理。
如果程序不满足assert 语句,则整个程序直接退出。而if 和 try-catch 则可以选择是否继续运行等。
举个例子,银行将顾客排队到窗口,如果没有空闲窗口,则此时应该用 if 或 try-catch ,try-catch 可以继续运行,让顾客等待或别的行为。而assert 会直接关闭整个银行。