1.3 捕获异常
就像前面提到的一样,C++异常处理机制的一个好处是,可以使程序员在一个地方专注于所要解决的问题,而在另一个地方对这段代码所产生的错误进行处理。
1.3.1 try块
如果在一个函数内部抛出了异常(或者被这个函数所调用的其他函数抛出了异常),这个函数将会因为抛出异常而退出。如果不想因为一个throw而退出函数,可以在函数中试图解决实际产生程序设计问题的地方(和可能产生异常的地方)设置一个try块。这个块被称做try块的原因是程序需要在这里尝试着调用各种函数。try块只是一个普通的程序块,由关键字try引导:
如果希望通过仔细地检查每一个被调用函数的返回值来发现错误,程序员必须围绕每一次函数调用编写初始化和检测代码,即使多次调用同一个函数也是如此。使用异常处理时,可以将所有工作放入try块中,然后在try块的后面处理可能产生的异常。这样一来,代码将更容易编写和阅读,因为代码的设计目标不会被错误处理所干扰。
1.3.2 异常处理器
当然,被抛出的异常肯定会在某个地方终止。这个地方就是异常处理器(exception handler),程序员需要为每一种想捕获的异常类型设置一个异常处理器。然而,多态对于异常同样有效,因此一个异常处理器可以处理某种异常类型和这种类型的派生类。
异常处理器紧接着try块,并且由关键字catch标识:
catch子句的语法类似于带有单一参数的函数。可以在异常处理器内部使用标识符(id1、id2等),就像使用函数的参数一样。如果不需要在异常处理器中使用标识符,这些标识符可以省略。异常类型通常提供了对其进行处理的足够的信息。
异常处理器都必须紧跟在try块之后。一旦某个异常被抛出,异常处理机制将会依次寻找参数类型与异常类型相匹配的异常处理器。找到第一个这样的异常处理器后,程序的执行流程进入这个catch子句,于是系统就可以认为该异常已经处理了。(查找异常处理器的过程在找到第一个匹配的catch子句之后就终止了。)只有匹配的catch子句才会执行;在执行完最后一个与该try块相关的异常处理器后,程序又恢复到正常的控制流程。
注意,在try块中,不同的函数调用可能产生相同类型的异常,这时,只需要一个异常处理器就可以了。
为了举例说明try和catch,我们在这里修改了Nonlocal.cpp,将其中的setjmp()用一个try块代替,将longjmp()用一个throw语句代替:
当执行函数oz()中的throw语句时,程序的控制流程开始回溯,直到找到某个带有int型参数的catch子句为止。程序在这个catch子句的主体中恢复运行。这个程序与Nonlocal.cpp的最重要的区别在于,当throw语句造成程序的执行过程从oz()函数返回时,对象rb的析构函数被调用。
1.3.3 终止和恢复
在异常处理理论中有两个基本的模型:终止和恢复。在终止(termination)(C++支持这种模型)模型中,假定错误非常严重,以至于不可能在异常发生的地点自动恢复程序的执行。也就是说,无论谁抛出一个异常,都表明程序已经陷入了无法挽救的困境,并且不需要再返回发生异常的地方。
另一个异常处理模型被称为恢复(resumption)模型,在20世纪60年代,PL/I语言首先引入该模型[1]。使用恢复模型意味着异常处理器希望能够校正这种情况,然后自动地重新执行发生错误的代码,并希望第二次执行能够成功。如果希望在C++中重新恢复程序的执行,则必须显式地将程序的执行流程转移到错误发生的地方,通常的做法是重新调用发生错误的函数。将try块放到while循环中,不断地重新执行try块中的程序,直到产生满意的结果,这种做法并不罕见。
历史上,使用支持恢复性异常处理模型的操作系统的程序员们,最终使用的是类似终止模型的代码并跳过了异常恢复。虽然恢复模型非常吸引人,但是在实践中并没有多大用处。其中一个原因或许就是发生异常的位置和异常处理器之间的距离。对于远处的异常处理器来说,终止执行是一个问题;而另一个问题是,对于一个大型系统来说,在很多位置都可能发生异常,从异常发生的位置跳转到远处的异常处理器然后再返回,这在概念上也十分困难。
[1]通过ON ERROR功能,BASIC语言长期以来支持一种有限形式的恢复性异常处理模型。