Exceptions¶
Basics¶
exceptions 提供了一种机制,可以将错误或其他异常情况的处理与代码的典型控制流分离。
这允许更自由地在对给定情况最有用的时间和方式处理错误,从而减轻返回代码导致的大部分(如果不是全部)混乱。
C++ 中的异常是使用三个相互配合的关键字来实现的:throw
、try
和catch
.
1) Throwing exceptions
在 C++ 中, throw
语句用于表示发生了异常或错误情况(考虑抛出惩罚标志)。发出异常已发生的信号通常也称为引发异常。
使用 throw
关键字,后跟您希望用来表示发生错误的任何数据类型的值。通常,该值是错误代码、问题描述或自定义异常类。
C++ | |
---|---|
1 2 3 4 5 |
|
这些语句中的每一个都充当一个信号,表明发生了某种需要处理的问题
2) Looking for exceptions
使用 try
关键字来定义语句块(称为 try
块)。 try
块充当观察者,查找 try
块中的任何语句引发的任何异常
C++ | |
---|---|
1 2 3 4 5 |
|
注意,try 块没有定义我们将如何处理异常。它只是告诉程序,“嘿,如果这个 try 块中的任何语句抛出异常,抓住它!”。
3) Handling exceptions
实际上处理异常是 catch
块的工作。 catch
关键字用于定义处理单个数据类型的异常的代码块(称为 catch
块)
C++ | |
---|---|
1 2 3 4 5 |
|
Try
块和 catch
块一起工作:
try
块检测try
块内的语句引发的任何异常,并将它们路由到具有匹配类型的catch
块进行处理- 一个
try
块必须至少有一个紧随其后的catch
块,但可以有多个按顺序列出的catch
块 - No type conversion is done for exceptions (so an
int
exception will not be converted to match a catch block with adouble
/long
parameter).
精确匹配的exception
当将异常与 catch
块匹配时,程序不会执行隐式转换或升级!我们采取的是绝对的精确匹配
例如,char
异常将与 int catch
块不匹配。 int
异常不会与 float catch
块匹配。
但是唯一的例外是:将执行从派生类到其父类之一的强制转换,这是唯一的特例!
例1: 这个例子想说明执行顺序
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
- 不能精确匹配
long
,不行! - 精确匹配
int
,行!
匹配成功,跳出剩余catch
步骤,直接执行cout << "hello" << endl;
例2: 这个例子想说明 Exceptions are handled immediately
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这个程序非常简单。发生的情况如下:throw
语句是第一个执行的语句 —— 这会导致引发 double
类型的异常。执行==立即移至==最近的封闭 try
块,这是该程序中唯一的 try
块。
然后检查 catch
处理程序以查看是否有任何处理程序匹配。我们的异常是 double
类型,因此我们正在寻找 double
类型的 catch
处理程序。我们有一个,所以它会执行。
因此打印输出:
Bash | |
---|---|
1 |
|
Note that “This never prints” is never printed, because the exception caused the execution path to jump immediately to the exception handler for doubles.
What catch blocks typically do
如果异常被路由到 catch
块,即使 catch
块为空,也会被视为“已处理”。 但是,通常您会希望 catch
块做一些有用的事情。
catch
块在捕获异常时会执行四种常见操作:
catch
块可能会打印错误(打印到控制台或日志文件),然后允许函数继续执行catch
块可能会向调用者返回一个值或错误代码catch
块可能会抛出另一个异常。由于catch
块位于try
块之外,因此在这种情况下新抛出的异常不会由前面的try
块处理,而是由下一个封闭的try
块处理 (“套娃”)main()
中的catch
块可用于捕获致命错误并以干净的方式终止程序
Advanced Exception View¶
1) Try
块不仅捕获来自 try
块内的语句的异常,还捕获来自 try
块内调用的函数的 exception
:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
2) Exception handling and stack unwinding:
- 当抛出异常时,程序首先查看当前函数内是否可以立即处理异常(意味着异常是在当前函数内的 try 块内抛出的,并且有一个相应的关联的 catch 块)。如果当前函数可以处理异常,它就会处理异常
- 如果没有,程序接下来检查函数的调用者(调用堆栈中的下一个函数)是否可以处理异常。为了让函数的调用者处理异常,对当前函数的调用必须位于 try 块内,并且必须关联一个匹配的 catch 块。如果未找到匹配项,则检查调用者的调用者(调用堆栈上的两个函数)。同样,为了让调用者的调用者处理异常,对调用者的调用必须位于 try 块内,并且必须关联一个匹配的 catch 块
- 检查调用堆栈中的每个函数的过程将继续进行,直到找到处理程序,或者检查了调用堆栈上的所有函数但找不到处理程序
- 如果找到匹配的异常处理程序,则执行将从抛出异常的位置跳转到匹配的 catch 块的顶部
- 如果没有找到匹配的异常处理程序,则堆栈可能会也可能不会展开 (后面会说)
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
打印输出:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
函数 B()
有两个独立的 try
块。包含对 C()
的调用的 try
块有一个 double
类型异常的处理程序,但这与我们的 int
类型异常不匹配(并且异常不进行类型转换),因此找不到匹配项。空的 try
块确实有一个用于 int
类型异常的异常处理程序,但此 catch
块不被视为匹配,因为对 C()
的调用不在关联的 try
块内。
3) Uncaught exceptions
当一个函数抛出一个它自己无法处理的异常时,它会假设调用堆栈中某处的函数将处理该异常。在下面的示例中,mySqrt() 假设有人将处理它引发的异常,但如果没有人真正处理会发生什么?
当找不到函数的异常处理程序时,将调用 std::terminate()
并终止应用程序。在这种情况下,调用堆栈可能会也可能不会展开!如果堆栈没有展开,局部变量将不会被销毁,并且在销毁所述变量时预期的任何清理都不会发生!
After Uncaught Exception
- 如果未处理异常,则调用堆栈可能会也可能不会展开
- 如果堆栈未展开,则局部变量将不会被销毁,如果这些变量具有重要的析构函数,则可能会导致问题
现在我们发现自己陷入了一个难题:
- 函数可能会引发任何数据类型(包括程序定义的数据类型)的异常,这意味着可以捕获无数可能的异常类型
- 如果未捕获异常,您的程序将立即终止(并且堆栈可能不会展开,因此您的程序甚至可能无法正确清理自身),这是我们想规避的
- 为每种可能的类型添加显式的 catch 处理程序是很乏味的,特别是对于那些预计仅在特殊情况下才能达到的类型
幸运的是,C++还为我们提供了捕获所有类型异常的机制。这称为:catch-all handler
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
因为 int 类型没有特定的异常处理程序,所以 catch-all 处理程序捕获此异常。该示例产生以下结果:
Bash | |
---|---|
1 |
|
catch-all
处理程序必须放置在catch
块链的最后。这是为了确保针对特定数据类型定制的异常处理程序存在时可以捕获异常- 通常,
catch-all
处理程序块留空 (这将捕获任何意外的异常,确保此时发生堆栈展开并防止程序终止)
C++ | |
---|---|
1 |
|
我们跳过书中的“27.5/6/7”
Exception dangers and downsides¶
新程序员在使用异常时遇到的最大问题之一就是异常发生时清理资源的问题
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
如果 WriteFile()
失败并抛出 FileException
会发生什么?此时,我们已经打开了文件,现在控制流跳转到 FileException
处理程序,该处理程序打印错误并退出。请注意,该文件从未关闭!这个例子应该重写如下:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
我们跳过书中的“27.9/10”