27-2 基本异常处理
在上一节关于异常处理必要性的课程中,我们讨论了使用返回码会导致控制流和错误处理流程混杂在一起,从而限制了两者。C++ 中的异常处理是通过三个关键字实现的,它们协同工作:throw、try和catch。
抛出异常
在现实生活中,我们经常使用信号来标记特定事件的发生。例如,在美式橄榄球比赛中,如果一名球员犯规,裁判会将旗帜扔在地上并吹哨终止比赛。然后裁判会判罚并执行犯规。一旦犯规处理完毕,比赛通常会恢复正常。
在 C++ 中,throw 语句用于表示发生了异常或错误情况(类似于抛出罚旗)。表示发生了异常通常也称为引发异常。
要使用 throw 语句,只需使用 throw 关键字,后跟一个用于指示发生错误的任意数据类型的值。通常,此值可以是错误代码、问题描述或自定义异常类。
以下是一些例子:
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
这些声明都表明出现了某种需要处理的问题。
捕捉异常情况
抛出异常只是异常处理过程的一部分。让我们回到美式橄榄球的例子:裁判吹罚犯规后,接下来会发生什么?球员们会注意到犯规并停止比赛。橄榄球比赛的正常进程被打断了。
在 C++ 中,我们使用try关键字来定义一个语句块(称为try代码块)。try 代码块充当观察者的角色,它会检查 try 代码块内任何语句抛出的任何异常。
以下是一个 try 代码块的示例:
try
{
// Statements that may throw exceptions you want to handle go here
throw -1; // here's a trivial throw statement
}
请注意,try 代码块并没有定义我们将如何处理异常。它只是告诉程序:“嘿,如果这个 try 代码块中的任何语句抛出异常,就捕获它!”
处理异常
最后,我们以美式橄榄球为例进行说明:判罚犯规后,比赛暂停,裁判会评估犯规并执行。换句话说,必须先处理好犯规,比赛才能恢复正常。
实际上,处理异常是 catch 代码块的职责。catch关键字用于定义一个代码块(称为catch 代码块),该代码块处理特定数据类型的异常。
以下是一个用于捕获整数异常的 catch 代码块示例:
catch (int x)
{
// Handle an exception of type int here
std::cerr << "We caught an int exception with value" << x << '\n';
}
try 代码块和 catch 代码块协同工作——try 代码块会检测其内部语句抛出的任何异常,并将其路由到具有匹配类型的 catch 代码块进行处理。try 代码块后必须至少紧跟一个 catch 代码块,但可以按顺序列出多个 catch 代码块。
一旦 try 代码块捕获到异常并将其路由到匹配的 catch 代码块进行处理,则该异常被视为已处理。匹配的 catch 代码块执行完毕后,程序将恢复正常执行,从最后一个 catch 代码块之后的第一条语句开始。
catch 参数的工作方式与函数参数类似,参数在后续的 catch 代码块中可用。基本类型的异常可以按值捕获,但非基本类型的异常应该按常量引用捕获,以避免不必要的复制(在某些情况下,还可以防止切片)。
与函数类似,如果参数不会在 catch 代码块中使用,则可以省略变量名:
catch (double) // note: no variable name since we don't use it in the catch block below
{
// Handle exception of type double here
std::cerr << "We caught an exception of type double\n";
}
这有助于防止编译器发出关于未使用变量的警告。
不会对异常进行类型转换(因此 int 异常不会转换为与带有 double 参数的 catch 块匹配)。
将抛出(throw)、捕捉(try)和处理(catch)结合起来
这是一个完整的程序,使用了throw、try和多个catch模块:
#include <iostream>
#include <string>
int main()
{
try
{
// Statements that may throw exceptions you want to handle go here
throw -1; // here's a trivial example
}
catch (double) // no variable name since we don't use the exception itself in the catch block below
{
// Any exceptions of type double thrown within the above try block get sent here
std::cerr << "We caught an exception of type double\n";
}
catch (int x)
{
// Any exceptions of type int thrown within the above try block get sent here
std::cerr << "We caught an int exception with value: " << x << '\n';
}
catch (const std::string&) // catch classes by const reference
{
// Any exceptions of type std::string thrown within the above try block get sent here
std::cerr << "We caught an exception of type std::string\n";
}
// Execution continues here after the exception has been handled by any of the above catch blocks
std::cout << "Continuing on our merry way\n";
return 0;
}
在作者的机器上,运行上述 try/catch 代码块会产生以下结果:

使用 throw 语句抛出了一个值为 -1 的异常,异常类型为 int。该 throw 语句随后被其外层的 try 代码块捕获,并被路由到相应的 catch 代码块,该 catch 代码块处理 int 类型的异常。该 catch 代码块打印了相应的错误信息。
异常处理完毕后,程序在 catch 代码块之后继续正常运行,并打印“继续我们的快乐旅程”。
异常处理概述
异常处理其实很简单,以下两段涵盖了您需要记住的大部分内容:
当抛出异常(使用throw 语句)时,程序会找到最近的包含该异常的try代码块(如有必要,会向上遍历调用栈查找包含该异常的 try 代码块——我们将在下一课详细讨论这一点),以查看该 try 代码块中是否有catch处理程序可以处理该类型的异常。如果可以,程序会跳转到 catch 代码块的顶部,此时该异常被认为已处理完毕。
如果最近的封闭 try 代码块中没有合适的 catch 处理程序,程序会继续查找后续的封闭 try 代码块,以寻找 catch 处理程序。如果在程序结束前仍找不到合适的 catch 处理程序,程序将抛出运行时异常错误。
请注意,程序在将异常与 catch 代码块匹配时,不会执行隐式转换或类型提升!例如,char 异常不会与 int 异常匹配,int 异常也不会与 float 异常匹配。但是,程序会执行从派生类到其父类的强制类型转换。
事情就是这样。本章余下部分将致力于展示这些原则在实践中的应用实例。
异常情况会立即处理。
以下是一个简短的程序,演示了如何立即处理异常:
#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 处理程序。找到了,所以它被执行。
因此,该程序的结果如下:

请注意,“This never prints”永远不会被打印出来,因为异常导致执行路径立即跳转到 double 类型的异常处理程序。
一个更贴近现实的例子
让我们来看一个不太学术化的例子:
#include <cmath> // for sqrt() function
#include <iostream>
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)
{
// 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*
// Otherwise, print the answer
std::cout << "The sqrt of " << x << " is " << std::sqrt(x) << '\n';
}
catch (const char* exception) // catch exceptions of type const char*
{
std::cerr << "Error: " << exception << '\n';
}
}
这段代码要求用户输入一个数字。如果用户输入的是正数,则 if 语句不会执行,不会抛出异常,而是打印出该数字的平方根。由于这种情况下没有抛出异常,因此 catch 代码块内的代码永远不会执行。结果类似于这样:

如果用户输入负数,我们会抛出一个 const char 类型的异常。由于我们位于 try 代码块中,并且找到了匹配的异常处理程序,控制权会立即转移到 const char异常处理程序。结果是:

现在,你应该已经掌握了异常处理的基本概念。在下一课中,我们将通过更多示例来展示异常处理的灵活性。
catch块通常会做什么
如果异常被路由到 catch 代码块,即使 catch 代码块为空,也被视为“已处理”。但是,通常情况下,您希望 catch 代码块执行一些有用的操作。catch 代码块在捕获异常时通常会执行以下四种操作:
首先,catch 块可以打印错误(打印到控制台或日志文件),然后允许函数继续执行。
其次,catch 代码块可以向调用者返回一个值或错误代码。
第三,catch 代码块可能会抛出另一个异常。由于 catch 代码块位于 try 代码块之外,因此这种新抛出的异常不会被前面的 try 代码块处理,而是由下一个包含 catch 代码块的 try 代码块处理。
第四,可以在 main() 函数中使用 catch 代码块来捕获致命错误,并以干净的方式终止程序。

浙公网安备 33010602011771号