异常、函数和堆栈展开
本节阅读量:在上一课中,我们解释了如何使用throw、try和catch来启用异常处理。在本课中,我们将讨论异常处理如何与函数交互。
被调函数抛出异常
在上一课中,我们注意到,“try块检测由try块内的语句抛出的任何异常”。在相应的示例中,我们的throw语句被放在try块中,并被关联的catch块捕获,所有这些都在同一函数中。在单个函数中同时抛出和捕获异常的价值有限。
更有趣的是,如果try块中的语句是函数调用,而被调用的函数抛出异常,会发生什么?try块是否会检测从try块内调用的函数所引发的异常?
幸运的是,答案是肯定的!
异常处理最有用的特性之一是throw语句不必直接放在try块中。相反,可以从被调函数中的任何位置引发异常,而调用方(或调用方的调用方,等等…)的try块可以捕获这些异常。当以这种方式捕获异常时,代码的执行流会从引发异常的点跳到处理异常的catch块。
关键点
try块不仅会捕获try块中语句引发的异常,还会捕获try块内调用的函数所引发的异常。
这允许我们以更模块化的方式使用异常处理。我们将通过重写上一课中的平方根程序来演示这一点。
|
|
在这个程序中,我们将检查异常并计算平方根的代码放在名为mySqrt()的函数中。然后,我们从try块内部调用这个mySqrt()函数。让我们验证它是否仍按预期工作:
|
|
是的!当mySqrt()中引发异常时,mySqrt()本身没有处理该异常。然而,对mySqrt()的调用(在main()中)位于带有匹配异常处理程序的try/catch中。因此,执行流会从mySqrt()中的throw语句跳到main()中catch块的开头,然后继续执行。
上述程序最有趣的地方在于,mySqrt()函数可以引发异常,但它本身不处理该异常!这本质上意味着mySqrt()愿意报告“这里有个问题!”,但不负责自己处理问题。它会将处理异常的责任委托给调用者(这类似于使用返回码,将处理错误的责任传回函数调用者)。
此时,您可能想知道为什么将错误传递回调用方是一个好主意。为什么不让mySqrt()处理自己的错误?问题是,不同的应用程序可能希望以不同方式处理错误。控制台应用程序可能希望打印文本消息。Windows应用程序可能希望弹出错误对话框。在一个应用程序中,出现的问题可能是致命错误,而在另一个应用程序中,可能不是。通过将错误传出函数,每个应用程序都可以按最适合自身上下文的方式处理来自mySqrt()的错误!最终,这会让mySqrt()尽可能保持模块化,并将错误处理放在代码中模块化程度较低的部分。
异常处理和堆栈展开
在这一节中,我们将了解涉及多个函数时,异常处理实际上是如何工作的。
当抛出异常时,程序首先查看该异常是否可以立即在当前函数内处理(这意味着该异常是在当前函数的try块内抛出的,并且有一个相关联的对应catch块)。如果当前函数可以处理异常,则会这样做。
如果不能,程序接下来会检查函数的调用方(调用栈上的上一个函数)是否可以处理异常。为了让函数的调用方处理异常,对当前函数的调用必须位于try块内,并且必须关联匹配的catch块。如果没有找到匹配项,则检查调用者的调用者(调用栈上再上一层的函数)。类似地,为了让调用方的调用方处理异常,对调用方的调用也必须位于try块内,并且必须关联匹配的catch块。
在调用栈上逐层检查函数的过程会持续进行,直到找到处理程序,或者检查完调用栈上的所有函数仍找不到处理程序。
如果找到匹配的异常处理程序,执行流会从引发异常的点跳到匹配catch块的开头。这需要按需多次展开堆栈(从调用栈中移除当前函数),直到处理异常的函数成为调用栈上当前最底层的函数。
如果找不到匹配的异常处理程序,则堆栈可能会展开,也可能不会展开。我们将在下一课中详细讨论这种情况。
当当前函数从调用栈中移除时,所有局部变量都会像往常一样销毁,但不会返回任何值。
关键点
展开堆栈会销毁被展开函数中的局部变量(这是必要的,因为它可以确保这些变量的析构函数被执行)。
一个堆栈展开示例
为了说明上面的内容,让我们看一个更复杂的例子,使用一个更大的堆栈。尽管这个程序很长,但它非常简单:main()调用A(),A()调用B(),B()调用C(),C()调用D(),D()抛出异常。
|
|
请更详细地看一看这个程序,尝试找出运行时会打印哪些内容、不会打印哪些内容。答案如下:
|
|
让我们看看在这种情况下会发生什么。所有“Start”语句的打印都很直接,不需要进一步解释。函数D()打印“D throwing int exception”,然后引发int异常。事情从这里开始变得有趣。
由于D()本身不处理异常,因此程序会检查其调用方(调用栈上的函数),查看是否有函数可以处理该异常。函数C()不处理任何异常,因此在那里找不到匹配项。
函数B()有两个单独的try块。包含对C()调用的try块具有用于double类型异常的处理程序,但它与int类型异常不匹配(并且异常不会进行类型转换),因此未找到匹配项。下面的空try块不会抛出异常。
A()也有一个try块,对B()的调用位于其中,因此程序会查看是否存在int异常的catch处理程序。存在!因此,A()会处理异常,并打印“A caught int exception”。
由于现在已经处理了异常,因此控制流在A()中的catch块之后正常继续。这意味着A()打印“End A”,然后正常终止。
控制流返回main()。尽管main()有一个int异常处理程序,但我们的异常已经由A()处理,因此main()中的catch块不会执行。main()只会打印“End main”,然后正常终止。
该程序说明了许多有趣的原则:
首先,如果抛出异常的函数的直接调用方不想处理异常,它就不必处理。在这种情况下,C()不处理D()抛出的异常,而是将该职责委托给调用栈上的上一层调用方。
其次,如果try块没有与被抛出异常类型对应的catch处理程序,则会发生堆栈展开,就像根本没有try块一样。在这种情况下,B()也没有处理异常,因为它没有正确类型的catch块。
第三,如果函数具有匹配的catch块,但对当前函数的调用没有在关联的try块中发生,则不会使用该catch块。我们在B()中也看到了这一点。
最后,一旦匹配的catch块执行完毕,控制流就会照常继续,从最后一个catch块之后的第一条语句开始。这一点通过A()处理错误、继续执行“End A”、然后返回调用方得到体现。当程序返回到main()时,异常已经被抛出并处理——main()完全不知道发生过异常!
正如您所看到的,堆栈展开为我们提供了一些非常有用的行为——如果函数不想处理异常,就不必处理。异常会沿调用栈向上传播,直到找到愿意处理它的地方!这允许我们决定调用栈中哪个位置最适合处理错误。
在下一课中,我们将看一看当您不捕获异常时会发生什么,以及防止这种情况发生的方法。