未捕获的异常和捕获所有异常
本节阅读量:现在,您应该已经对异常的工作方式有了比较清晰的了解。在本课中,我们将介绍几个更有趣的异常情况。
未捕获的异常
当函数抛出自己不处理的异常时,它会假设调用栈中的某个函数将处理该异常。在下面的示例中,mySqrt()假设有人会处理它抛出的异常——但如果实际上没有人处理,会发生什么?
下面是我们的sqrt程序,其中移除了main()中的try块:
|
|
现在,假设用户输入-4,mySqrt(-4)引发异常。函数mySqrt()不处理异常,因此程序会查看调用栈中的其他函数是否会处理该异常。main()也没有针对该异常的处理程序,因此找不到任何处理代码。
当找不到异常处理程序时,会调用std::terminate(),并终止应用程序。在这种情况下,调用栈可能会展开,也可能不会展开!如果调用栈未展开,则不会销毁局部变量,销毁变量时预期发生的任何清理都不会执行!
当异常未处理时,操作系统通常会通知您发生了未处理异常错误。通知方式取决于操作系统,可能包括打印错误消息、弹出错误对话框,或者只是让程序崩溃。有些操作系统的处理方式不太优雅。通常,这是我们希望避免的情况!
警告
如果未处理异常,则调用栈可能会展开,也可能不会展开。
如果调用栈未展开,则不会销毁局部变量,如果这些变量具有非平凡的析构函数,则可能会导致问题。
旁白
尽管在这种情况下不展开调用栈似乎很奇怪,但这样做有一个充分理由。未处理异常通常是我们希望尽力避免的。如果调用栈被展开,那么异常发生时调用栈状态中的调试信息都会丢失!通过不展开调用栈,我们可以保留这些信息,从而更容易确定是什么引发了未处理异常,并修复它。
捕获所有异常
现在我们发现自己陷入了一个难题:
- 函数可能会引发任何数据类型(包括用户定义的数据类型)的异常,这意味着要捕获的可能异常类型数量是无限的。
- 如果未捕获异常,则程序将立即终止(并且调用栈可能不会展开,因此您的程序甚至可能不会在自身正确清理后结束)。
- 为每个可能的类型添加显式catch处理程序是冗长的!
幸运的是,C++还提供了一种捕获所有类型异常的机制,称为捕获所有异常的处理程序(catch all)。它的工作方式与普通catch块类似,只是它不使用特定类型来捕获,而是使用「…」作为要捕获的类型。因此,catch all处理程序有时也称为“省略号捕获处理程序”。
当用作函数参数时,省略号以前用于将任何类型的参数传递给函数。在catch的上下文中,它们表示任何数据类型的异常。下面是一个简单的例子:
|
|
因为int类型没有特定的异常处理程序,所以catch all处理程序会捕获此异常。此示例产生以下结果:
|
|
catch all处理程序必须放在catch链的最后。这是为了确保为特定数据类型定制的异常处理程序有机会先捕获异常。
通常,catch all处理程序块为空:
|
|
这会捕获任何未预料到的异常,确保在此之前发生调用栈展开,并防止程序终止,但不会进行特定的错误处理。
使用catch all处理程序包装main
catch all处理程序的一个用途是包装main()函数:
|
|
在这种情况下,如果runGame()或它调用的任何函数抛出未处理异常,该异常将被这个catch all处理程序捕获。调用栈会以有序方式展开(确保局部变量被销毁)。这也会防止程序立即终止,让我们有机会打印自定义错误,并在退出之前保存用户状态。
提示
如果您的程序使用异常,请考虑在main中使用catch all处理程序,以便在发生未处理异常时帮助确保行为有序。
如果catch all处理程序捕获到异常,则应假设程序现在处于某种不确定状态,需要立即执行清理,然后终止。
调试未处理的异常
未处理异常表示发生了意外情况,我们可能希望首先诊断引发未处理异常的原因。许多调试器会(或可以配置为)在未处理异常处中断,允许我们在引发未处理异常的位置查看调用栈。然而,如果存在catch-all处理程序,那么所有异常都会被处理,并且(因为调用栈被展开)我们会丢失有用的诊断信息。
因此,在调试构建中,禁用catch all处理程序可能很有用。我们可以通过条件编译指令实现这一点。
这里有一种方法:
|
|
在语法上,try块需要至少一个关联的catch块。因此,如果catch all处理程序是条件编译出来的,我们要么需要有条件地编译try块,要么需要有条件地编译另一个catch块。后者更简洁。
为此,我们创建了类DummyException。该类不能被实例化,因为它有一个已删除的默认构造函数,并且没有其他构造函数。当未定义NDEBUG时,我们编译一个捕获DummyException类型异常的catch处理程序。因为我们不能创建DummyException,所以这个catch处理程序永远不会捕获任何东西。因此,任何到达这一点的异常都不会被处理。