概观
本章介绍异常处理,这是 C++ 用于报告和恢复程序中意外事件的机制。到本章结束时,您将能够识别适合异常处理的事件类型;知道何时抛出异常,何时返回错误代码;使用异常处理编写健壮的代码;使用带有异常处理的 RAII 在意外事件后自动回收资源;并从意外事件中恢复并继续执行。
前几章介绍了 C++ 控制流语句和变量声明。我们已经尝到了面向对象编程的滋味,并从动态变量中创建了数据结构。在本章中,我们将注意力转向 C++ 如何帮助开发人员处理程序中意外出错时出现的情况。
用户输入的无效数字、等待响应的意外超时以及逻辑错误都是程序中事件的例子。这些事件中的一些,比如输入错误,可能会频繁或可预测地发生,因此必须预测和处理它们,否则程序将无法使用。其他的事件,比如超时,很少发生,也从来不会发生在程序和运行它的系统工作正常的时候。还有一些事件,比如逻辑错误,根本就不应该发生,但有时它们确实会发生。
用户输入错误事件是预期事件。用特定的代码处理;可能会显示一个“用户输入错误”对话框,程序将返回等待再次输入。从用户输入错误中恢复的代码很可能在词汇上接近检测到错误的代码,因为它的操作强烈依赖于发生的特定事件。普通的控制流语句适用于处理预期事件。处理完预期事件后,代码可以继续正常执行,就像事件没有发生一样。
逻辑错误是一个意外事件。不可能编写特定的代码来处理意外事件,因为这些事件实际上是意外的——它们不应该发生。
无法编写特定代码来处理意外事件的另一个原因是,每个语句中都可能发生大量意外事件。每个函数调用都可能有逻辑错误、参数错误和运行时错误。如果您必须编写特定的代码来处理每个可能发生的事件,那么程序将是 99.9%的事件处理程序,什么也做不了。
无法编写处理意外事件的特定代码的第三个原因是因为这些事件阻止了程序的前进。程序无法修复逻辑错误(因为它是意外的),因此它无法通过逻辑错误测试。这使得程序处理意外事件的方法数量有限。
程序可能会暂停,它可能会重试一段代表某种计算的代码,以查看意外事件是否消失,或者它可能会放弃包含意外事件的计算,并尝试做其他事情。这些处理动作相对通用。每个动作可能适合许多不同的意外事件。
C++ 异常处理是为意外事件设计的,即响应以下事件:
- 不经常和不可预测地发生
- 阻止程序向前推进
当然,您可以对预期的事件使用异常处理,但它不是这项工作的合适工具。您也可以使用异常处理从函数返回,但是向同事解释会更慢更难。异常处理不适合这些工作,就像锤子不适合拧螺丝一样。用锤子敲螺丝是可能的,但是这样做是困难且低效的。
意外事件可能在程序中的任何地方被检测到,但它们通常在与操作系统和外部世界交互的库函数中被检测到。对这些函数的调用通常嵌套在函数调用堆栈的许多层中。
意外事件会阻止程序当前计算的前进。当遇到意外事件时,程序可以选择突然停止,但是如果它想做除了停止之外的任何事情(包括简单地保存工作和打印消息),它必须放弃当前的计算,并返回到启动新计算的更高级代码。正是在这个更高级别的代码中,程序可以决定执行是可以继续还是必须停止。
这有两种可能发生的方式。传统上,检测到意外事件的函数可以停止它正在做的事情,手动清理它正在使用的任何资源,并向它的调用方返回一个错误代码。调用者反过来清理并将错误代码返回给调用者。错误代码像桶旅一样一步一步地沿着调用链向上传递,直到它到达能够响应它的代码,如下图所示:
图 13.1:可视化错误代码的逐步返回
错误代码的逐步返回充满了风险。如果函数没有捕获所有被调用函数的返回代码,它可能会尝试继续,而不是将错误代码传递给它的调用方,如下图所示:
图:13.2:中级函数删除错误代码
试图在意外事件后继续执行通常会导致越来越严重的意外事件级联,直到操作系统强制停止程序。如果一个函数不删除动态变量、关闭打开的文件句柄和释放其他资源,这些资源就会泄漏,导致程序或操作系统最终变得不稳定并崩溃。
将执行返回到高级代码的另一种方法是使用 C++ 异常处理。异常处理有三个部分。一个throw语句“抛出”一个异常,表明一个意外事件的发生。C++ 运行时系统“解绕”函数调用堆栈,调用每个局部变量的析构函数,而不返回对包含局部变量的函数的控制。然后一个try/catch块“捕获”异常,结束展开过程并允许继续执行。
图 13.3:可视化异常的抛出和捕获
抛出的异常不能像返回的错误代码一样被忽略。异常要么被try/catch块捕获,要么 C++ 运行时系统终止程序。
当 C++ 运行时系统在处理抛出的异常时展开堆栈,它调用所有局部变量的析构函数。封装在智能指针或 C++ 类中的资源被删除,因此不会泄漏。开发人员不必编写复杂的控制流代码来处理正常执行情况和意外错误情况,就像他们在逐步返回错误代码时必须做的那样。这些特性使得异常处理成为处理意外事件的更好方法。
一个throw语句抛出一个异常,向 C++ 运行时系统发出发生了意外事件的信号。throw语句由throw关键字和任意类型的表达式组成。引发的异常与表达式的类型相同。C++ 提供了一个异常类型库,这些异常类型是从std::exception派生出来的类实例,但是一个程序并不局限于抛出这些或者其他任何类实例;一个程序可以抛出一个int或者一个char*或者任何其他想要的类型。以下是一些throw语句的例子:
-
抛出
std::exception类型的异常:throw std::exception; -
抛出
std::logic_error类型的异常,这是一个从std::exception派生的类。该异常有一个描述特定异常的可选文本字符串:throw std::logic_error("This should not be executed");
-
抛出
std::runtime_error类型的异常,这是一个从std::exception派生的类。异常有一个整数错误代码和一个可选的文本字符串,该字符串进一步描述了特定的异常。整数代码是操作系统特有的错误:throw std::runtime_error(LastError(), "in OpenFile()");
-
抛出 Linux
errno伪变量作为int类型的异常。整数值是特定于操作系统的错误代码:throw errno; -
抛出
char const*类型的异常。字符串的内容描述了异常:throw "i before e except after c";
开发人员将使用的大多数标准异常要么是std::logic_error及其派生词,要么是std::runtime_error及其派生词,尤其是std::system_error。其余的标准异常由 C++ 标准库函数抛出。标准的意图从来都不清楚为什么一个例外是logic_error,另一个是runtime_error,还有一个两者都不是。这根本不是 C++ 设计得更好的部分之一。
在内存不足的情况下,使用标准异常是有问题的,因为大多数标准异常在构造时可能会分配动态变量。标准异常的what参数没有定义的含义。只是插入到std::exception的what()成员函数返回的字符串中的一点文字。
异常由throw语句抛出,并被try/catch块中的catch子句捕获。我们将在后面研究捕捉异常。
如果抛出的异常没有被try/catch块捕获,C++ 运行时系统将终止程序。抛出异常比调用exit()或abort()终止程序执行要好,因为它记录了一个意外事件已经发生。抛出异常还允许程序在以后通过捕获异常并决定是终止程序还是继续来改进。
在本练习中,我们将看到当我们抛出一个未被try/catch块的catch子句捕获的异常时会发生什么:
注意
练习的完整代码可以在这里找到:https://packt.live/37tOlS4。
-
在
main()功能的框架中键入。看起来是这样的:#include <iostream> using namespace std; int main() { return 0; }
-
在
main()内,插入throw语句。这是由throw关键字后跟任意类型的表达式组成的。异常通常是从 C++ 标准库std::exception类派生的类实例,但是任何类型的表达式都可以。您可以抛出一个整数,如错误号,甚至是描述异常的空终止文本字符串:throw "An exception of some type";
-
完成的程序如下所示:
#include <iostream> using namespace std; int main() { throw "An exception of some type"; return 0; }
-
Run the program. While the precise message printed varies by operating system and compiler, the output from the tutorialspoint online compiler looks like this:
图 13.4:练习 80 中程序的输出
这里发生了什么?当抛出异常而未被捕获时,C++ 运行时系统调用标准库
terminate()函数。terminate()不返回;而是导致程序退出,向操作系统发出异常终止的信号。在 Linux 上,这种异常终止会转储一个核心文件进行调试。 -
在
using namespace std;之后,添加一个名为deeply_nested()的int函数。功能骨架如下图:int deeply_nested() { return 0; }
-
添加代码抛出
int值123。然后输出"in deeply_nested after throw"。完成的功能如下:int deeply_nested() { throw 123; cout << "in deeply_nested() after throw" << endl; return 0; }
-
在
deeply_nested()之后,添加另一个名为intermediate()的int函数。它的骨架是这样的:int intermediate() { return 0; }
-
给
deeply_nested()增加一个通话。不要忘记在名为rc的int变量中捕获deeply_nested()的返回值(代表返回代码)。输出消息"in intermediate(), after deeply_nested()"。然后,从deeply_nested()返回rc中的返回码。完整的功能如下:int intermediate() { int rc = deeply_nested(); cout << "in intermediate(), after deeply_nested()"; return rc; }
-
在
main()中,将throw语句替换为对intermediate()的调用。不要从intermediate():intermediate();获取退货代码
-
更新后的程序如下:
```cpp
#include <iostream>
using namespace std;
int deeply_nested()
{
throw 123;
cout << "in deeply_nested() after throw" << endl;
return 0;
}
int intermediate()
{
int rc = deeply_nested();
cout << "in intermediate(), after deeply_nested()";
return rc;
}
int main()
{
intermediate();
return 0;
}
```
- 运行程序。虽然打印的确切消息因操作系统和编译器而异,但 tutorialspoint 在线编译器的输出如下所示:
图 13.5:练习 80 中更新程序的输出
这里发生了什么?main()叫intermediate(),也就是叫deeply_nested()。这是为了表示程序的正常行为,当抛出异常时,程序通常执行嵌套多层的函数。当执行throw语句时,deeply_nested()中代码的执行停止。C++ 运行时系统开始寻找一个try/catch块来捕获异常。deeply_nested()没有,intermediate()没有,main()也没有,所以 C++ 运行时系统调用terminate()。输出由操作系统产生,并且可能因操作系统或编译器版本而异。注意throw后面的输出语句都没有执行,表示throw语句后功能停止执行。
注意
这个程序需要注意的另一件事是返回代码。deeply_nested()返回一个代码,可能描述一个错误。intermediate()也是。但是main()没有捕获intermediate()的返回代码,所以如果没有抛出异常,错误信息就会丢失。如果异常没有被捕获,它们会可靠地停止程序,并且它们会可靠地将错误信息从throw语句传输到catch子句的位置。
捕捉异常的代码称为try/catch块。它由两部分组成;try块由try关键字组成,后跟一个用花括号括起来的语句列表,是由try/catch块控制的语句块。在try区块之后是一个或多个catch条款。每个catch子句由catch关键字组成,后跟一个带圆括号的变量声明,声明要被catch子句捕获的异常类型。最后一个catch子句可能是catch (...),它捕捉以前没有捕捉到的每个异常。
这里有一个样本try/catch块:
try
{
auto p = make_unique<char[]>(100);
}
catch (std::exception& e)
{
cout << e.what() << endl;
}try块包含单个语句:
auto p = make_unique<char[]>(100);如果没有足够的内存来创建动态变量,该语句可能会引发类型为std::bad_alloc的异常。执行try块中的语句。如果没有异常发生,执行将继续执行最后一个catch子句之后的语句。
样本try/catch块中有一个catch子句,用于处理任何类型源自std::exception的异常,包括std::bad_alloc。如果出现异常,该异常的类型将与第一个catch子句的类型进行比较。如果异常的类型可以构造或初始化catch子句的变量,实际函数参数构造形式参数的方式,则catch子句开始执行。示例catch子句中的可执行语句使用std::exception::what()成员函数打印出异常的描述:
cout << e.what() << endl;执行catch子句中的复合语句后,该异常被视为已处理。在最后一个catch条款之后的位置继续执行。
如果抛出异常的类型不能构造第一个catch子句中的变量,则比较下一个catch子句。catch条款的顺序很重要。C++ 运行时系统从上到下匹配针对catch子句抛出的异常。第一个catch条款抓住了例外,在这个条款中可以构造例外。catch条款的顺序应该从最具体到最一般,最一般的catch (...)在列表的最后。
如果没有catch子句与抛出异常的类型匹配,则在包围try/catch块的范围(由花括号分隔)内继续搜索try/catch块。
这个练习展示了try/catch方块的基本形式。try/catch块的目的是处理一些或所有抛出的异常,以决定程序的执行是否可以继续:
注意
练习的完整代码可以在这里找到:https://packt.live/2sa34l1。
-
进入
main()功能的骨架。代码如下:#include <iostream> using namespace std; int main() { return 0; }
-
输入上一练习中的功能
deeply_nested()。代码如下:int deeply_nested() { throw 123; return 0; }
-
在
main()内部,创建一个try/catch块。在try街区内,呼叫deeply_nested()。添加一个catch块,使用 catch 子句catch(...)捕获所有异常。在catch块内,输出"in catch ..."弦。代码如下:try { deeply_nested(); } catch (...) { cout << "in catch ..." << endl; }
-
try/catch闭塞后,输出"in main(), after try/catch"串。代码如下:cout << "in main(), after try/catch" << endl; -
完整的程序如下:
#include <iostream> using namespace std; int deeply_nested() { throw 123; return 0; } int main() { try { deeply_nested(); } catch (...) { cout << "in catch ..." << endl; } cout << "in main(), after try/catch" << endl; return 0; }
-
运行程序。它的输出如下所示:
图 13.6:练习 81 中程序的输出
请注意,程序没有调用terminate(),也没有以操作系统的异常终止消息结束。main()叫做deeply_nested()。deeply_nested()中抛出的异常被catch(...)条款捕获,该条款打印了消息"in catch ..。”,所以正常的程序执行继续在 main()的catch子句后并打印出消息"in main(), after try/catch"。
不要删除程序。下一个练习将在此基础上进行。
C++ 标准库的某些 C++ 语句和某些函数会引发异常。C++ 语句和函数抛出的所有异常都是从std::exception派生的类的实例,可以在<exception>头中找到。源自std::exception的异常的一个有用特性是,它们提供了成员函数,您可以调用这些函数来获取关于异常的更多信息。要访问这些成员函数,一个catch子句必须将捕获的异常分配给一个变量。
捕捉类型为std::exception的异常并将对该异常的引用放入名为e的变量中的catch子句是:
catch (std::exception& e)您可能已经通过使用catch语句的值捕捉到了相同的异常:
catch (std::exception e)但这需要复制例外。抛出的异常存在于为此目的保留的内存中,因此不需要动态变量来保存异常。这很重要,因为 C++ 抛出的一个异常是bad_alloc异常,它发生在内存无法分配的时候。复制异常可能需要创建一个动态变量,如果内存不足,会导致程序崩溃。
并非每个异常都是由开发人员自己的代码中的throw语句引发的。C++ 语句和标准库函数会引发一些异常。在本练习中,我们将捕捉由 C++ 标准库函数引发的异常:
注意
练习的完整代码可以在这里找到:https://packt.live/2KNtmQy。
-
从上一个练习的完整程序开始。如果需要重新输入,看起来是这样的:
#include <iostream> using namespace std; int deeply_nested() { throw 123; return 0; } int main() { try { deeply_nested(); } catch (...) { cout << "in catch ..." << endl; } cout << "in main(), after try/catch" << endl; return 0; }
-
在表头
<iostream>的include下方,为<exception>增加一个include,为<string>增加一个include。这是代码:#include <exception> #include <string>
-
In
deeply_nested(), replace thethrowstatement with the following statement:string("xyzzy").at(100);
这个语句的作用是创建一个标准的库字符串,将其初始化为一个五个字母的单词,然后请求字符串的第 100 个字符。显然这是不可能的,所以
at()成员函数抛出异常。到目前为止,程序如下所示:
#include <iostream> #include <exception> #include <string> using namespace std; int deeply_nested() { string("xyzzy").at(100); return 0; } int main() { try { deeply_nested(); } catch (...) { cout << "in catch ..." << endl; } cout << "in main(), after try/catch" << endl; return 0; }
-
Run the program. Its output is the same as that of the previous exercise:
图 13.7:练习 82 中程序的输出
-
在
main()中的catch(...)条款之前,增加一个新的catch条款。抓住参考变量e中的exception类型(记住,这是std::exception,因为using namespace std语句)。在catch子句中,输出e.what()返回的值,该值打印描述异常的文本字符串。新的catch条款如下:catch (exception& e) { cout << "caught " << e.what() << endl; }
-
更新后的程序如下:
#include <iostream> #include <exception> #include <string> using namespace std; int deeply_nested() { string("xyzzy").at(100); return 0; } int main() { try { deeply_nested(); } catch (exception& e) { cout << "caught " << e.what() << endl; } catch (...) { cout << "in catch ..." << endl; } cout << "in main(), after try/catch" << endl; return 0; }
-
Run the program. It produces the following output:
图 13.8:练习 82 中修订程序的输出
main()称为deeply_nested()。在deeply_nested()内部,语句string("xyzzy").at(100);抛出了一个源自std::exception类型的异常。什么类型是例外?它是类out_of_range的一个实例,类logic_error派生自类exception。该例外首先与std::exception&匹配。对派生类的引用可以初始化对基类的引用,所以这个catch子句被执行,产生第一行输出。捕捉到异常后,执行继续执行
try/catch块后面的行,如预期的那样,该行打印第二个输出行。没有执行catch(...)子句,因为 C++ 已经将抛出的异常与之前的catch子句进行了匹配,并执行了catch子句的语句。 -
如果程序在
catch (exception& e)子句之前包含了logic_error或out_of_range的catch子句,那么catch子句就会被执行。但是如果catch (exception& e)条款首先出现,它就会被执行。请记住,catch条款是按顺序检查的。执行与异常匹配的第一个catch子句,而不是最佳匹配的子句。
展开栈是销毁栈上每个作用域的局部变量,寻找一个try/catch块的过程。
下面是 C++ 运行时系统在处理抛出的异常时所做的事情。展开堆栈从最里面的动态嵌套范围开始。这是throw语句周围的范围(用花括号分隔)。之所以称之为动态嵌套作用域,是因为在程序执行过程中,当一个函数调用另一个函数时,函数堆栈上的函数作用域堆栈会动态变化。
对于函数激活堆栈上的每个作用域,C++ 运行时系统执行以下步骤,只要有更多的作用域,就重复这些步骤:
- 当前范围内的所有局部变量都将被销毁。C++ 精确地跟踪每个作用域中需要销毁的变量。如果正在构造的类引发异常,则只会销毁已经构造的基类和成员变量。如果一个块中只有一些变量被构造,那么只有那些变量被销毁。
- 如果当前范围是一个函数范围,函数的激活记录将从堆栈中弹出,C++ 运行时系统将处理下一个封闭范围。
- 如果当前范围不是
try块,C++ 继续处理下一个封闭范围。 - 否则,当前范围是一个
try块。C++ 运行时系统依次将每个catch子句与抛出异常的类型进行比较。如果抛出异常的类型可以构造成catch子句中的变量,catch子句变量的构造方式与函数形式参数相同,则执行catch子句。然后,紧接着最后一个catch块继续执行语句,该过程完成。 - 如果抛出的异常不能被构造到任何
catch子句中,C++ 将处理下一个封闭范围。 - 如果没有(更多)作用域,则不捕获异常。C++ 运行时系统调用
terminate(),然后将控制权返回给操作系统,表示异常终止状态。
毫无疑问,C++ 异常的堆栈展开行为非常强大。C++ 标准库定义了许多类,它们获取资源,拥有这些资源,并在类实例被破坏时释放这些资源。这个习惯用法,即一个类拥有一个资源并在删除时释放它,被称为 RAII(资源获取就是初始化)。我们在第 8 章中看到的智能指针是删除自己拥有的动态变量的 RAII 类。在作用域中拥有资源的任何智能指针或其他 RAII 类实例都会在作用域退出之前释放这些资源,这样资源就不会泄漏。
异常处理和 RAII 的结合将开发人员从编写删除所拥有资源的两条不同路径中解放出来:当执行成功时遵循一条路径,当意外事件发生时遵循第二条路径。开发人员只需要使用智能指针和 C++ 标准库的其他 RAII 类。C++ 关于在离开作用域时销毁对象的规则和 RAII 类的行为自动管理资源的释放,无需显式编码。
下一个练习使用带有噪声类实例的代码来演示堆栈展开过程。
在本练习中,我们将创建一个调用函数来产生动态嵌套变量范围的程序。程序抛出一个异常来说明堆栈展开过程是如何发生的:
注意
练习的完整代码可以在这里找到:https://packt.live/2pGj9xP。
-
进入骨架
main()功能。代码如下:#include <iostream> using namespace std; int main() { return 0; }
-
添加标题
<exception>和<memory>库的include指令。这个程序抛出一个源自std::exception的异常,需要<memory>:#include <exception> #include <memory>
中定义的智能指针
-
输入我们老朋友类的定义
noisy。代码如下:class noisy { char const* s_; public: noisy(char const* s) { cout << "constructing " << (s_ = s) << endl; } ~noisy() { cout << "destroying " << s_ << endl; } };
-
输入
int功能deeply_nested()。它的骨架是这样的:int deeply_nested() { return 0; }
-
在
deeply_nested()中,使用make_unique()创建一个指向动态noisy变量的智能指针。代码如下:auto n = make_unique<noisy>("deeply_nested");
-
抛出一个逻辑错误。
logic_error接受空终止的字符串构造函数参数。这可以是你喜欢的任何东西;试试"totally illogical":throw logic_error("totally illogical");
-
进入
int功能intermediate()。它的骨架是这样的:int intermediate() { return 0; }
-
创建类
noisy的本地实例。它的论点可以是"intermediate"。类的析构函数noisy打印一条消息。它是在析构函数中显式释放资源的类的替身:noisy n("intermediate");
-
给
deeply_nested()增加一个通话。在int变量rc中捕获deeply_nested()的返回值。输出消息"after calling deeply_nested"。返回rc:int rc = deeply_nested(); cout << "after calling deeply_nested()" << endl; return rc;
-
在功能
main()中,添加一个try/catch块。它应该有一个用于类异常的catch条款。try/catch街区的骨架是这样的:
```cpp
try
{
}
catch (exception& e)
{
}
```
- 在
try块中,使用构造函数参数"try in main":
```cpp
auto n = make_unique<noisy>("try in main");
```
构造一个指向动态`noisy`实例的智能指针
- 调用
intermediate()并打印退货代码:
```cpp
int rc = intermediate();
cout << "intermediate() returned " << rc << endl;
```
- 在
catch子句中,输出e.what(),这样我们就知道捕捉到了什么异常:
```cpp
cout << "in catch: exception: " << e.what() << endl;
```
try/catch块后,输出字符串"ending main()":
```cpp
cout << "ending main" << endl;
```
- 完成的程序如下所示:
```cpp
#include <iostream>
#include <exception>
#include <memory>
using namespace std;
class noisy
{
char const* s_;
public:
noisy(char const* s) { cout << "constructing " << (s_ = s) << endl; }
~noisy() { cout << "destroying " << s_ << endl; }
};
int deeply_nested()
{
auto n = make_unique<noisy>("deeply_nested");
throw logic_error("totally illogical");
return 0;
}
int intermediate()
{
noisy n("intermediate");
int rc = deeply_nested();
cout << "after calling deeply_nested()" << endl;
return rc;
}
int main()
{
try
{
auto n = make_unique<noisy>("try in main");
int rc = intermediate();
cout << "intermediate() returned " << rc << endl;
}
catch (exception& e)
{
cout << "in catch: exception: " << e.what() << endl;
}
cout << "ending main" << endl;
return 0;
}
```
- Compile and run the program. Its output looks like this:

图 13.9:练习 83 中程序的输出
`main()`中的`try`块构造了`noisy`的动态实例(输出的第一行)。`main()`称为`intermediate()`,代表多层函数调用。`intermediate()`构造了一个`noisy`的实例(第二行)。`intermediate()`称为`deeply_nested()`,构造了一个动态的`noisy`实例(第三行)。此时,函数调用堆栈帧如下所示:

图 13.10:函数调用堆栈
`deeply_nested()`抛出了一个异常。我们知道发生这种情况是因为`deeply_nested()`中的`noisy`实例被破坏了(第四行),但是`intermediate()`中的输出语句没有被执行。`intermediate()`中没有`try/catch`区块,所以`intermediate()`中的`noisy`实例被破坏了(第五行)。`main()`中有一个`try/catch`块。`try/catch`块中`noisy`的动态实例被破坏(第六行)。异常的类型为`std::logic_error`,由`std::exception`派生而来。有`catch`条款为例外,故执行`catch`条款(第七行)。执行继续到`try/catch`块后的输出语句(第八行)。
注意`deeply_nested()`和`intermediate()`返回值并抛出异常。因为引发了异常,所以没有执行返回该值的代码。
- 在这个非常理想的自动退卷过程中,有一个不幸的“陷阱”。用
try块的内容替换main()中的try/catch块,使main()看起来像这样:
```cpp
int main()
{
auto n = make_unique<noisy>("try in main");
int rc = intermediate();
cout << "intermediate() returned " << rc << endl;
cout << "ending main" << endl;
return 0;
}
```
- Compile and run the program again. If you see the same output, breathe a sigh of relief. However, you may see output similar to the following:

图 13.11:练习 83 中修改后程序的输出
没有一个noisy实例被销毁。发生了什么事?事实证明,C++ 标准允许实现在未捕获到异常时不展开堆栈。
注意
每一个使用异常处理的程序都应该在main()的内容周围放置至少一个最小的try/catch块,这样一个意外的异常会打开堆栈。
在本章的总结活动中,您将探索程序如何捕捉异常,并在发生意外事件后选择继续还是终止程序执行。
想象你想写一个程序,一遍又一遍地做一些任意的事情,直到一些你无法控制的终止条件发生。您可以调用名为do_something()的bool函数来执行程序的操作。只要do_something()返回true你就继续,当do_something()最终返回false时结束程序。您的程序可能如下所示:
#include <iostream>
using namespace std;
int main()
{
bool continue_flag;
do
{
continue_flag = do_something();
}
while (continue_flag == true);
return 0;
}好吧,那很简单。让我们提高赌注。
假设你的程序所做的事情是监控一个 200 兆瓦核电反应堆的安全运行。你的do_something()功能现在叫做reactor_safety_check()。它读取传感器并设置控制器,以防止反应堆爆炸并辐射英国伦敦。这是一个非常重要的程序,无论如何都需要继续运行。该程序只有在感应到控制棒被一路推入且堆芯温度低于 100℃时才能停止运行,此时reactor_safety_check()返回false。
作为首席主回路软件工程师,您已经了解到实施反应堆安全检查代码的团队选择在运行时错误上抛出std::runtime_error异常,例如故障读取传感器。你的电气工程师向你保证,这些错误只是暂时的小故障。即使程序的一次迭代报告了runtime_error异常,下一次通过时,错误也很可能不会出现。
因为你心存疑虑,所以你在源代码中搜索throw语句,结果令你沮丧的是,发现有几个。你不知道这些其他例外意味着什么,但是如果不被抓住,它们带来的风险显然是严重的。你知道另一个叫做SCRAM()的功能,它一路推动控制棒,排出蒸汽,启动紧急给水泵,这是反应堆不受控制时你能做的一切。
你妈妈和你姐姐住在伦敦,所以即使你不想承担这个责任,你也不敢辞职。由你来防止堆芯熔毁或更严重的事件,核反应堆工程师委婉地称之为“迅速临界快速拆卸”,这意味着一个小的热核爆炸。
编写一个重复调用bool函数reactor_safety_check()的程序。继续main循环,处理runtime_error异常。通过调用SCRAM()并退出来处理其他异常。
注意
练习的完整代码可以在这里找到:https://packt.live/33chUEq。
以下是完成活动的一些步骤:
-
编写
reactor_safety_check()的测试版本,偶尔抛出异常来测试你的代码。这里有一个写reactor_safety_check()的提示。如果创建一个名为count的静态int变量,并在每次调用reactor_safety_check()时递增count,则可以使用count来决定在reactor_safety_check()中做什么。例如,也许你想在每次调用reactor_safety_check()时抛出一个小故障异常。 -
你会想要捕捉所有可能的异常,而不仅仅是
std::runtime_error,因为你不想在反应器还在运行的时候就终止循环。 -
You can assume that after you call
SCRAM(), you don't have to monitor the reactor any longer because there's nothing else that can be done. This is typical of error recovery actions, which take place on a best-effort basis.注意
这个活动的解决方案可以在第 574 页找到。
向程序通知意外事件的传统方式是使用检测到事件的函数的错误返回代码。这种方式充满风险,因为开发人员并不总是记得检查返回代码。异常克服了这个风险,因为异常要么被捕获,要么终止程序。
C++ 异常处理的特性旨在处理程序执行过程中的意外事件。
抛出的异常展开堆栈,在展开时调用每个作用域中每个变量的析构函数。使用 RAII 习惯用法,拥有资源(如动态变量、打开的文件句柄、互斥体等)的类可以释放这些资源。因为资源是在堆栈展开时释放的,所以在捕获异常后继续程序执行是安全的。
一个try/catch块可以捕捉异常。catch子句可以选择继续或停止程序执行。
这是本书的结尾,c++ Workshop,但这只是你学习的开始。在前几章中,我们描述了 C++ 的控制流语句。如果您希望能够使用这些语句,您将需要练习编写程序,并在一个在线 C++ 编译器或您选择的 C++ IDE 上自行执行它们。这本书向你介绍了几个概念和相关练习。然而,只有通过反复练习,你才能充分欣赏和利用你所学到的技能。
我们看了 C++ 中变量的基本类型,但也有变化。例如,int类型有short int、long int和long long int三个变种,加上所有这些的无符号变体。浮点类型有三种:float、double和long double。有数组和结构要尝试,还有带有成员函数的类。
动态变量让你在内存中构建任意大的数据结构,只要你避开了动态变量的死罪。智能指针和 RAII 将在这方面帮助你。
如果您以前的编程经验不包括对象,您可能需要几年时间才能适应面向对象编程。整本书都是关于这个主题的。
C++ 有一个标准库,包含打包为模板函数和类的算法和数据结构。在这本入门书中,我们没有空间教授模板编程,但是一旦你对基础知识感到满意,这是一个非常值得学习的主题。
您使用了 C++ 输出语句,但实际上只是触及了 C++ 输入/输出流接口的表面。它具有独特的功能和灵活性,因此值得进一步了解。
C++ 异常处理是一个强大的工具,它被压缩成两个语句。这是值得掌握的,有很多我们无法告诉你。
事实上,几乎每个 C++ 语句、声明、表达式和指令都有我们无法覆盖的皱纹。C++ 标准本身运行超过 1500 页。我们建议您在练习时查阅陈述,看看还有哪些额外的知识可以帮助您。这是即使是非常有经验的 C++ 开发人员都会做的事情,所以不要羞于不断提高自己的知识。
要多久你才有一个熟练工的 C++ 知识?不到一周——即使有我们优秀的书。只靠自学,大多数人需要全职两年的练习才能达到自己舒服的程度。作者认为你完成这本书有一个很好的开端,但是继续练习。







