异常是在程序执行的过程中发生了某种错误,异常的处理机制允许我们讲发生的异常抛出给程序的另外一部分,对这个错误进行处理。这个机制让问题检测的环节和问题处理的环节分离。检测环节只需要负责检测即可,无需关系解决的细节问题。在C语言中处理错误的方式主要是错误码,错误码的本质是对错误信息进行了分类编号,拿到错误码以后还要再去查询错误信息,比较麻烦。异常抛出时,抛出的是一个对象,这个对象可以包涵更多的信息。
程序在出现问题的时候,我们可以通过throw一个对象来引发一个异常,该对象的类型以及当前的调用链决定了这个异常会被哪个catch捕捉到。异常会被当前调用链中与该异常类型匹配且离抛出异常位置最近的那应该catch捕捉到。当throw被执行以后,throw后面的代码就不再执行。程序的执行逻辑会从throw的位置跳转到捕捉到这个异常的那个catch的位置,这个catch可能是当前函数的一个局部的catch,也可以是调用链中另外一个函数中的catch,程序的控制权就从throw的位置转移到了catch的位置,也就是说:在这个调用链上的函数可能会提早结束,同时在处理异常时,在调用链上从throw位置到catch位置之间创建的对象都会被销毁。
抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的这个异常对象可能是一个局部对象,同时处理这个异常的catch也不在当前的函数中,所以会生成一个临时对象,这个拷贝的对象会在catch语句后销毁。(这里类似于函数的传值返回)
一、栈展开
抛出异常以后,程序会暂停当前函数的执行,首先程序会检查当前的throw是否在try块的内部,如果在就开始查找匹配的catch语句,如果当前的catch不匹配,则会沿着调用链向上继续寻找,直到找到匹配的catch语句或者达到main函数也没有找到匹配的catch语句,此时程序会调用terminate函数终止程序,如果找到了匹配的catch语句后,执行完catch块内的语句后,会从这个catch语句后继续执行,这个过程就被叫做栈展开。
二、查找匹配的处理代码
一般情况下抛出对象和catch的类型是完全匹配的,如果有多个匹配的类型,那么会选择离他距离最近的那一个。
但是也有一些例外,允许从非常量向常量的类型转化,也就是权限的缩小;允许数组转化成指向数组元素类型的指针,函数被转换成指向这个函数的指针;允许从派生类向基类类型的转换。
然后到了main函数这个异常都没有被匹配就会终止程序,为了防止这种的情况的发生,我们一般会在main函数的最后使用catch(...),这样catch可以捕捉任意类型的异常,但是并不能知道捕捉到的异常错误是什么。
三、异常重新抛出
有的时候catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要特殊的,其他的错误则重新抛出异常给外层的调用链处理。捕捉异常后需要重新抛出直接throw就可以把捕捉到的这个异常直接抛出。
四、异常安全问题
异常抛出后,后面的代码就不再执行,前面申请的资源会在后面进行释放,但是可能中间会抛出异常,这样就会导致后面释放资源的代码不会执行,就因为异常引发了资源泄漏的问题,为了解决这个问题我们可以用到RAII的方式,后面会讲的智能指针就是利用了这种思想。
其次在析构函数中,如果需要抛出异常也要谨慎处理,比如析构函数中要释放十个资源,但是释放到第五个资源的时候抛出异常了,此时后面的五个资源就不会被释放了,就也造成了资源泄漏的问题。
五、异常规范
对于用户和编译器而言,预先知道某个程序会不会抛出异常是很有好处的,有助于简化调用函数的代码。
在C++98中在函数参数列表的后面接上throw(),表示这个函数不会抛出异常,函数参数列表后接上throw(类型1,类型2...)表示这个函数可能会抛出多种类型的异常,可能会抛出的异常类型用逗号分割。
C++11只需要在函数参数列表后面加上noexcept表示不会抛出异常,什么都不加表示可能会抛出异常。
编译器在编译时并不会检查noexcept,也就是说一个函数用noexcept修饰了。但是它又同时包含了throw语句或者它调用的函数可能会抛出异常,编译器还是会顺利通过的。但是如果用noexcept声明过的函数抛出了异常,程序就会终止。
noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回true,不会就返回false。