跳转至

Exceptions

Basics

exceptions 提供了一种机制,可以将错误或其他异常情况的处理与代码的典型控制流分离。

这允许更自由地在对给定情况最有用的时间和方式处理错误,从而减轻返回代码导致的大部分(如果不是全部)混乱。

C++ 中的异常是使用三个相互配合的关键字来实现的:throwtrycatch.

1) Throwing exceptions

在 C++ 中, throw 语句用于表示发生了异常或错误情况(考虑抛出惩罚标志)。发出异常已发生的信号通常也称为引发异常。

使用 throw 关键字,后跟您希望用来表示发生错误的任何数据类型的值。通常,该值是错误代码、问题描述或自定义异常类。

C++
1
2
3
4
5
throw -1; // throw a literal integer value
throw ENUM_INVALID_INDEX; // throw an enum value
throw "Can not take square root of negative number"; // throw a literal C-style (const char*) string
throw dX; // throw a double variable that was previously defined
throw MyException("Fatal Error"); // Throw an object of class MyException

这些语句中的每一个都充当一个信号,表明发生了某种需要处理的问题

2) Looking for exceptions

使用 try 关键字来定义语句块(称为 try 块)。 try 块充当观察者,查找 try 块中的任何语句引发的任何异常

C++
1
2
3
4
5
try
{
    // Statements that may throw exceptions you want to handle go here
    throw -1; // here's a trivial throw statement
}

注意,try 块没有定义我们将如何处理异常。它只是告诉程序,“嘿,如果这个 try 块中的任何语句抛出异常,抓住它!”。

3) Handling exceptions

实际上处理异常是 catch 块的工作。 catch 关键字用于定义处理单个数据类型的异常的代码块(称为 catch 块)

C++
1
2
3
4
5
catch (int x)
{
    // Handle an exception of type int here
    std::cerr << "We caught an int exception with value" << x << '\n';
}

Try 块和 catch 块一起工作:

  1. try 块检测 try 块内的语句引发的任何异常,并将它们路由到具有匹配类型的 catch 块进行处理
  2. 一个 try 块必须至少有一个紧随其后的 catch 块,但可以有多个按顺序列出的 catch
  3. No type conversion is done for exceptions (so an int exception will not be converted to match a catch block with a double/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
try 
{
    throw 1;
}
catch (long x)
{
    cerr << "We caught a long exception with value" << endl;
}
catch (int x)
{
    cerr << "int catched" << endl;
}
catch (double x)
{
    cerr << "double catched" << endl;
}

cout << "hello" << endl;
  1. 不能精确匹配long,不行!
  2. 精确匹配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
#include <iostream>

int main()
{
    try
    {
        throw 4.5; // throw exception of type double
        std::cout << "This never prints\n";
    }
    catch (double x) // handle exception of type double
    {
        std::cerr << "We caught a double of value: " << x << '\n';
    }

    return 0;
}

这个程序非常简单。发生的情况如下:throw 语句是第一个执行的语句 —— 这会导致引发 double 类型的异常。执行==立即移至==最近的封闭 try 块,这是该程序中唯一的 try 块。

然后检查 catch 处理程序以查看是否有任何处理程序匹配。我们的异常是 double 类型,因此我们正在寻找 double 类型的 catch 处理程序。我们有一个,所以它会执行。

因此打印输出:

Bash
1
We caught a double of value: 4.5

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 块在捕获异常时会执行四种常见操作:

  1. catch 块可能会打印错误(打印到控制台或日志文件),然后允许函数继续执行
  2. catch 块可能会向调用者返回一个值或错误代码
  3. catch 块可能会抛出另一个异常。由于 catch 块位于 try 块之外,因此在这种情况下新抛出的异常不会由前面的 try 块处理,而是由下一个封闭的 try 块处理 (“套娃”)
  4. 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
#include <cmath> // for sqrt() function
#include <iostream>

// A modular square root function
double mySqrt(double x)
{
    // If the user entered a negative number, this is an error condition
    if (x < 0.0)
        throw "Can not take sqrt of negative number"; // throw exception of type const char*

    return std::sqrt(x);
}

int main()
{
    std::cout << "Enter a number: ";
    double x {};
    std::cin >> x;

    try // Look for exceptions that occur within try block and route to attached catch block(s)
    {
        double d = mySqrt(x);
        std::cout << "The sqrt of " << x << " is " << d << '\n';
    }
    catch (const char* exception) // catch exceptions of type const char*
    {
        std::cerr << "Error: " << exception << std::endl;
    }

    return 0;
}

2) Exception handling and stack unwinding:

  1. 当抛出异常时,程序首先查看当前函数内是否可以立即处理异常(意味着异常是在当前函数内的 try 块内抛出的,并且有一个相应的关联的 catch 块)。如果当前函数可以处理异常,它就会处理异常
  2. 如果没有,程序接下来检查函数的调用者(调用堆栈中的下一个函数)是否可以处理异常。为了让函数的调用者处理异常,对当前函数的调用必须位于 try 块内,并且必须关联一个匹配的 catch 块。如果未找到匹配项,则检查调用者的调用者(调用堆栈上的两个函数)。同样,为了让调用者的调用者处理异常,对调用者的调用必须位于 try 块内,并且必须关联一个匹配的 catch 块
  3. 检查调用堆栈中的每个函数的过程将继续进行,直到找到处理程序,或者检查了调用堆栈上的所有函数但找不到处理程序
    • 如果找到匹配的异常处理程序,则执行将从抛出异常的位置跳转到匹配的 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
#include <iostream>

void D() // called by C()
{
    std::cout << "Start D\n";
    std::cout << "D throwing int exception\n";

    throw - 1;

    std::cout << "End D\n"; // skipped over
}

void C() // called by B()
{
    std::cout << "Start C\n";
    D();
    std::cout << "End C\n";
}

void B() // called by A()
{
    std::cout << "Start B\n";

    try
    {
        C();
    }
    catch (double) // not caught: exception type mismatch
    {
        std::cerr << "B caught double exception\n";
    }

    try
    {
    }
    catch (int) // not caught: exception not thrown within try
    {
        std::cerr << "B caught int exception\n";
    }

    std::cout << "End B\n";
}

void A() // called by main()
{
    std::cout << "Start A\n";

    try
    {
        B();
    }
    catch (int) // exception caught here and handled
    {
        std::cerr << "A caught int exception\n";
    }
    catch (double) // not called because exception was handled by prior catch block
    {
        std::cerr << "A caught double exception\n";
    }

    // execution continues here after the exception is handled
    std::cout << "End A\n";
}

int main()
{
    std::cout << "Start main\n";

    try
    {
        A();
    }
    catch (int) // not called because exception was handled by A
    {
        std::cerr << "main caught int exception\n";
    }
    std::cout << "End main\n";

    return 0;
}

打印输出:

C++
1
2
3
4
5
6
7
8
9
Start main
Start A
Start B
Start C
Start D
D throwing int exception
A caught int exception
End A
End main

函数 B() 有两个独立的 try 块。包含对 C() 的调用的 try 块有一个 double 类型异常的处理程序,但这与我们的 int 类型异常不匹配(并且异常不进行类型转换),因此找不到匹配项。空的 try 块确实有一个用于 int 类型异常的异常处理程序,但此 catch 块不被视为匹配,因为对 C() 的调用不在关联的 try 块内。

3) Uncaught exceptions

当一个函数抛出一个它自己无法处理的异常时,它会假设调用堆栈中某处的函数将处理该异常。在下面的示例中,mySqrt() 假设有人将处理它引发的异常,但如果没有人真正处理会发生什么?

当找不到函数的异常处理程序时,将调用 std::terminate() 并终止应用程序。在这种情况下,调用堆栈可能会也可能不会展开!如果堆栈没有展开,局部变量将不会被销毁,并且在销毁所述变量时预期的任何清理都不会发生!

After Uncaught Exception
  1. 如果未处理异常,则调用堆栈可能会也可能不会展开
  2. 如果堆栈未展开,则局部变量将不会被销毁,如果这些变量具有重要的析构函数,则可能会导致问题

现在我们发现自己陷入了一个难题:

  1. 函数可能会引发任何数据类型(包括程序定义的数据类型)的异常,这意味着可以捕获无数可能的异常类型
  2. 如果未捕获异常,您的程序将立即终止(并且堆栈可能不会展开,因此您的程序甚至可能无法正确清理自身),这是我们想规避的
  3. 为每种可能的类型添加显式的 catch 处理程序是很乏味的,特别是对于那些预计仅在特殊情况下才能达到的类型

幸运的是,C++还为我们提供了捕获所有类型异常的机制。这称为:catch-all handler

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
    try
    {
        throw 5; // throw an int exception
    }
    catch (double x)
    {
        std::cout << "We caught an exception of type double: " << x << '\n';
    }
    catch (...) // catch-all handler
    {
        std::cout << "We caught an exception of an undetermined type\n";
    }
}

因为 int 类型没有特定的异常处理程序,所以 catch-all 处理程序捕获此异常。该示例产生以下结果:

Bash
1
We caught an exception of an undetermined type
  1. catch-all 处理程序必须放置在 catch 块链的最后。这是为了确保针对特定数据类型定制的异常处理程序存在时可以捕获异常
  2. 通常,catch-all 处理程序块留空 (这将捕获任何意外的异常,确保此时发生堆栈展开并防止程序终止)
C++
1
catch(...) {} // ignore any unanticipated exceptions

我们跳过书中的“27.5/6/7”

Exception dangers and downsides

新程序员在使用异常时遇到的最大问题之一就是异常发生时清理资源的问题

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

try
{
    openFile(filename);
    writeFile(filename, data);
    closeFile(filename);
}
catch (const FileException& exception)
{
    std::cerr << "Failed to write to file: " << exception.what() << '\n';
}

如果 WriteFile() 失败并抛出 FileException 会发生什么?此时,我们已经打开了文件,现在控制流跳转到 FileException 处理程序,该处理程序打印错误并退出。请注意,该文件从未关闭!这个例子应该重写如下:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>

try
{
    openFile(filename);
    writeFile(filename, data);
}
catch (const FileException& exception)
{
    std::cerr << "Failed to write to file: " << exception.what() << '\n';
}

// Make sure file is closed
closeFile(filename);

我们跳过书中的“27.9/10”