C++ 异常处理:理论、栈展开与最佳实践
C++学习阶段的三个参考文档
看库文件(非官方文档): Cplusplus.com
这个文档在 C++98、C++11 时候还行,之后就完全没法用了……
准官方文档(同步更新)——还 可以看语法:C++准官方参考文档
这个行,包括 C++26 都同步了,我们以后主要会看这个。
官方文档(类似论坛): Standard C++
这个网站上面会有很多大佬,类似于论坛。
1 ~> 异常的概念
在 C 语言里面,异常的处理机制——通过错误码的形式处理错误,比较麻烦。异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。
C 语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出一个对象(可以抛出任何类型的异常),这个对象可以函数更全面的各种信息(包含各种各样的信息)。
2 ~> 异常的使用层
2.1 异常的抛出和捕获
程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个 catch 的处理代码来处理该异常。
被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从 throw 位置跳到与之匹配的 catch 模块,catch 可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数中的 catch,控制权从 throw 位置转移到了 catch 位置。
抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在 catch 子句后销毁(这里的处理类似于函数的传值返回)。
2.2 栈展开
2.2.1 理论
抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的 catch 子句,首先检查 throw 本身是否在 try 块内部,如果在则查找匹配的 catch 语句,如果有匹配的,则跳到 catch 的地方进行处理。
如果当前函数中没有 try/catch 子句,或者有 try/catch 子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的 catch 过程被称为栈展开。
如果到达 main 函数,依旧没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止程序。
如果找到匹配的 catch 子句处理后,catch 子句后面的代码会继续执行。
2.2.2 最佳实践
注意:e.what(); 返回包含异常的字符串。
上图中,[ LINE ] 的作用:获取你是哪个位置的宏。
2.3 查找匹配的处理代码
2.3.1 抛出对象和 catch 一般是类型完全匹配的
一般情况下抛出对象和 catch 是类型完全匹配的,如果有多个类型匹配的,就选择离它位置更近的那个(类型匹配时遵循'就近原则')。
但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。
如果到 main 函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般 main 函数中最后都会使用 catch(…),它可以捕获任意类型的异常,但是我们不知道异常错误是什么。


