27-4 未捕获的异常和兜底处理程序
现在,你应该对异常的工作原理有了大致的了解。在本课中,我们将介绍一些更有趣的异常情况。
未捕获的异常
当一个函数抛出它自身无法处理的异常时,它假定调用栈中某个更下游的函数会处理该异常。在下面的例子中,mySqrt() 函数假定某个函数会处理它抛出的异常——但如果实际上没有函数处理该异常会发生什么呢?
这是我们的平方根程序,去掉了 main() 函数中的 try 代码块:
#include <iostream>
#include <cmath> // for sqrt() function
// 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;
// Look ma, no exception handler!
std::cout << "The sqrt of " << x << " is " << mySqrt(x) << '\n';
return 0;
}

假设用户输入 -4,而 mySqrt(-4) 函数抛出了一个异常。由于 mySqrt() 函数本身没有处理这个异常,程序会查找调用栈中是否有其他函数可以处理该异常。main() 函数也没有针对此异常的处理程序,因此找不到合适的处理程序。
当找不到函数的异常处理程序时,会调用 std::terminate() 并终止应用程序。在这种情况下,调用栈可能被展开,也可能不会!如果调用栈没有被展开,局部变量将不会被销毁,因此任何预期在销毁这些变量时进行的清理工作都不会发生!
警告:
如果未处理异常,调用堆栈可能会也可能不会被展开。
如果堆栈没有展开,局部变量就不会被销毁,如果这些变量具有非平凡的析构函数,则可能会造成问题。
顺便提一下:
虽然在这种情况下不展开堆栈看起来很奇怪,但这样做是有充分理由的。未处理的异常通常是我们应该不惜一切代价避免的。如果展开堆栈,那么所有关于导致抛出未处理异常的堆栈状态的调试信息都会丢失!通过不展开堆栈,我们可以保留这些信息,从而更容易确定未处理异常是如何抛出的,并进行修复。
当异常未被处理时,操作系统通常会通知您发生了未处理的异常错误。具体通知方式取决于操作系统,可能包括打印错误消息、弹出错误对话框或直接崩溃。有些操作系统的处理方式不如其他操作系统优雅。一般来说,您应该尽量避免这种情况!
兜底处理程序
现在我们陷入了困境:
函数可能会抛出任何数据类型(包括程序定义的数据类型)的异常,这意味着需要捕获的异常类型有无限多种。
如果异常未被捕获,程序将立即终止(并且堆栈可能不会被展开,因此程序甚至可能无法正确地清理自身)。
为每种可能的类型添加显式的 catch 处理程序很繁琐,尤其是对于那些预计只会在特殊情况下才会执行的处理程序而言!
幸运的是,C++ 也提供了一种机制来捕获所有类型的异常。这被称为“捕获所有异常处理程序”(catch-all handler)。捕获所有异常处理程序的工作方式与普通的 catch 块类似,不同之处在于它不使用特定的类型来捕获异常,而是使用省略号运算符 (…) 作为捕获类型。因此,捕获所有异常处理程序有时也被称为“省略号捕获处理程序”。
如果你还记得第20.5 课——省略号(以及为什么要避免使用它们)的内容,省略号以前用于向函数传递任何类型的参数。而在这里,它们表示任何类型的异常。以下是一个简单的例子:
#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(...) {} // ignore any unanticipated exceptions
这将捕获任何意外异常,确保堆栈展开到此为止,防止程序终止,但不会进行任何具体的错误处理。
使用捕获所有问题的处理程序来包装 main() 函数。
捕获所有问题的处理程序的一个用途是包装 main() 函数的内容:
#include <iostream>
struct GameSession
{
// Game session data here
};
void runGame(GameSession&)
{
throw 1;
}
void saveGame(GameSession&)
{
// Save user's game here
}
int main()
{
GameSession session{};
try
{
runGame(session);
}
catch(...)
{
std::cerr << "Abnormal termination\n";
}
saveGame(session); // save the user's game (even if catch-all handler was hit)
return 0;
}

在这种情况下,如果 runGame() 或其调用的任何函数抛出未被处理的异常,该异常将被此捕获异常处理程序捕获。堆栈将按顺序展开(确保局部变量被销毁)。这也能防止程序立即终止,使我们有机会在退出前打印我们选择的错误信息并保存用户状态。
提示:
如果你的程序使用了异常处理,请考虑在 main 函数中使用一个捕获所有异常的处理程序,以帮助确保在发生未处理的异常时程序能够有序运行。
如果捕获异常的处理程序捕获到异常,则应假定程序现在处于某种不确定状态,立即执行清理,然后终止程序。
调试未处理的异常
未处理的异常表明发生了意外情况,我们通常需要诊断出抛出未处理异常的根本原因。许多调试器会在未处理的异常处中断(或者可以配置为中断),从而允许我们查看抛出异常时的堆栈信息。但是,如果我们使用了一个捕获所有异常的处理程序,那么所有异常都会被处理,并且(由于堆栈被展开),我们会丢失一些有用的诊断信息。
因此,在调试版本中,禁用通用处理程序可能很有用。我们可以通过条件编译指令来实现这一点。
以下是一种方法:
#include <iostream>
struct GameSession
{
// Game session data here
};
void runGame(GameSession&)
{
throw 1;
}
void saveGame(GameSession&)
{
// Save user's game here
}
class DummyException // a dummy class that can't be instantiated
{
DummyException() = delete;
};
int main()
{
GameSession session {};
try
{
runGame(session);
}
#ifndef NDEBUG // if we're in release node
catch(...) // compile in the catch-all handler
{
std::cerr << "Abnormal termination\n";
}
#else // in debug mode, compile in a catch that will never be hit (for syntactic reasons)
catch(DummyException)
{
}
#endif
saveGame(session); // save the user's game (even if catch-all handler was hit)
return 0;
}

从语法上讲,一个 try 代码块至少需要一个关联的 catch 代码块。因此,如果 catch 语句被条件性地编译掉,我们要么需要条件性地编译掉这个 try 代码块,要么需要条件性地编译加入另一个 catch 代码块。后一种方法更简洁。
为此,我们创建了一个DummyException无法实例化的类,因为它有一个被删除的默认构造函数,并且没有其他构造函数。在NDEBUG定义该类时,我们编译加入了一个 catch 处理程序来捕获类型为 Exception 的异常DummyException。由于我们无法创建 Exception 对象DummyException,因此这个 catch 处理程序永远不会捕获到任何异常。所以,任何到达此处的异常都不会被处理。

浙公网安备 33010602011771号