章节目录

未捕获的异常和捕获所有异常

本节阅读量:

现在,您应该已经对异常的工作方式有了比较清晰的了解。在本课中,我们将介绍几个更有趣的异常情况。


未捕获的异常

当函数抛出自己不处理的异常时,它会假设调用栈中的某个函数将处理该异常。在下面的示例中,mySqrt()假设有人会处理它抛出的异常——但如果实际上没有人处理,会发生什么?

下面是我们的sqrt程序,其中移除了main()中的try块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <cmath> // for sqrt()

// 独立的平方根函数
double mySqrt(double x)
{
    // 如果输入小于0,那么肯定是异常情况
    if (x < 0.0)
        throw "Can not take sqrt of negative number"; // 抛出类型为 const char* 的异常

    return std::sqrt(x);
}

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

    // 这里,不再处理异常
    std::cout << "The sqrt of " << x << " is " << mySqrt(x) << '\n';

    return 0;
}

现在,假设用户输入-4,mySqrt(-4)引发异常。函数mySqrt()不处理异常,因此程序会查看调用栈中的其他函数是否会处理该异常。main()也没有针对该异常的处理程序,因此找不到任何处理代码。

当找不到异常处理程序时,会调用std::terminate(),并终止应用程序。在这种情况下,调用栈可能会展开,也可能不会展开!如果调用栈未展开,则不会销毁局部变量,销毁变量时预期发生的任何清理都不会执行!

当异常未处理时,操作系统通常会通知您发生了未处理异常错误。通知方式取决于操作系统,可能包括打印错误消息、弹出错误对话框,或者只是让程序崩溃。有些操作系统的处理方式不太优雅。通常,这是我们希望避免的情况!


捕获所有异常

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

  1. 函数可能会引发任何数据类型(包括用户定义的数据类型)的异常,这意味着要捕获的可能异常类型数量是无限的。
  2. 如果未捕获异常,则程序将立即终止(并且调用栈可能不会展开,因此您的程序甚至可能不会在自身正确清理后结束)。
  3. 为每个可能的类型添加显式catch处理程序是冗长的!

幸运的是,C++还提供了一种捕获所有类型异常的机制,称为捕获所有异常的处理程序(catch all)。它的工作方式与普通catch块类似,只是它不使用特定类型来捕获,而是使用「…」作为要捕获的类型。因此,catch all处理程序有时也称为“省略号捕获处理程序”。

当用作函数参数时,省略号以前用于将任何类型的参数传递给函数。在catch的上下文中,它们表示任何数据类型的异常。下面是一个简单的例子:

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

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

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

1
We caught an exception of an undetermined type

catch all处理程序必须放在catch链的最后。这是为了确保为特定数据类型定制的异常处理程序有机会先捕获异常。

通常,catch all处理程序块为空:

1
catch(...) {} // 忽略任何非预期的异常

这会捕获任何未预料到的异常,确保在此之前发生调用栈展开,并防止程序终止,但不会进行特定的错误处理。


使用catch all处理程序包装main

catch all处理程序的一个用途是包装main()函数:

 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
#include <iostream>

struct GameSession
{
    // Game session 数据
};

void runGame(GameSession&)
{
    throw 1;
}

void saveGame(GameSession&)
{
    // 保存用户的游戏信息
}

int main()
{
    GameSession session{};

    try
    {
        runGame(session);
    }
    catch(...)
    {
        std::cerr << "Abnormal termination\n";
    }

    saveGame(session); // 即使发生了异常,也要保存用户游戏信息

    return 0;
}

在这种情况下,如果runGame()或它调用的任何函数抛出未处理异常,该异常将被这个catch all处理程序捕获。调用栈会以有序方式展开(确保局部变量被销毁)。这也会防止程序立即终止,让我们有机会打印自定义错误,并在退出之前保存用户状态。


调试未处理的异常

未处理异常表示发生了意外情况,我们可能希望首先诊断引发未处理异常的原因。许多调试器会(或可以配置为)在未处理异常处中断,允许我们在引发未处理异常的位置查看调用栈。然而,如果存在catch-all处理程序,那么所有异常都会被处理,并且(因为调用栈被展开)我们会丢失有用的诊断信息。

因此,在调试构建中,禁用catch all处理程序可能很有用。我们可以通过条件编译指令实现这一点。

这里有一种方法:

 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
#include <iostream>

struct GameSession
{
    // Game session 数据
};

void runGame(GameSession&)
{
    throw 1;
}

void saveGame(GameSession&)
{
    // 保存用户的游戏信息
}

class DummyException // 不能被初始化的类
{
    DummyException() = delete;
}; 

int main()
{
    GameSession session {}; 

    try
    {
        runGame(session);
    }
#ifdef NDEBUG // 发布给外部用的版本
    catch(...) // 编译 catch-all 
    {
        std::cerr << "Abnormal termination\n";
    }
#else // debug 版本, 编译一个永远不会走到的catch处理(为了兼容必须有catch关键字的语法)
    catch(DummyException)
    {
    }
#endif

    saveGame(session); // 即使发生了异常,也要保存用户游戏信息

    return 0;
}

在语法上,try块需要至少一个关联的catch块。因此,如果catch all处理程序是条件编译出来的,我们要么需要有条件地编译try块,要么需要有条件地编译另一个catch块。后者更简洁。

为此,我们创建了类DummyException。该类不能被实例化,因为它有一个已删除的默认构造函数,并且没有其他构造函数。当未定义NDEBUG时,我们编译一个捕获DummyException类型异常的catch处理程序。因为我们不能创建DummyException,所以这个catch处理程序永远不会捕获任何东西。因此,任何到达这一点的异常都不会被处理。


27.2 异常、函数和堆栈展开

上一节

27.4 异常、类和继承

下一节