异常
程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。例如,程序可能试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。通常,程序员都会试图预防这种意外情况。C++异常为处理这种情况提供了一种功能强大而灵活的工具。异常是相对较新的 C++ 功能,有些老式编译器可能没有实现。另外,有些编译器默认关闭这种特性,您可能需要使用编译器选项来启用它。
讨论异常之前,先来看看程序员可使用的一些基本方法。作为试验,以一个计算两个数的调和平均数的函数为例。两个数的调和平均数的定义是:这两个数字倒数的平均值的倒数,因此表达式为: 2.0 × x × y / ( x + y ) 2.0\times x\times y / (x+y) 2.0×x×y/(x+y)
如果 y 是 x 的负值,则上述公式将导致被零除——一种不允许的运算。对于被零除的情况,很多新式编译器通过生成一个表示无穷大的特殊浮点值来处理,cout 将这种值显示为 Inf、inf、INF 或类似的东西;而其他的编译器可能生成在发生被零除时崩溃的程序。最好编写在所有系统上都以相同的受控方式运行的代码。
调用 abort()
对于这种问题,处理方式之一是,如果其中一个参数是另一个参数的负值,则调用 abort() 函数。Abort() 函数的原型位于头文件 cstdlib(或 stdlib.h)中,其典型实现是向标准错误流(即 cerr 使用的错误流)发送消息 abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort() 是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用 exit(),该函数刷新文件缓冲区,但不显示消息。下面是一个使用 abort() 的小程序。
// error1.cpp -- using the abort() function
#include<iostream>
#include<cstdlib>
double hmean(double a, double b);
int main(){
double x, y, z;
std::cout << "Enter two numbers: ";
while( std::cin >> x >> y){
z = hmean(x,y);
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}
double hmean(double a, double b){
if(a == -b){
std::cout << "untenable arguments to hmean()\n";
std::abort();
}
return 2.0 * a * b / (a + b);
}
上述程序的运行情况如下:
Enter two numbers: 3 6
Harmonic mean of 3 and 6 is 4
Enter next set of numbers <q to quit>: 10 -10
untenable arguments to hmean()
注意,在 hmean() 中调用 abort() 函数将直接终止程序,而不是先返回到 main()。一般而言,显示的程序异常中断消息随编译器而异,下面是另一种编译器显示的消息:
This application has requested the Runtime to terminate it
in an unusual way. Please contact the application's support
team for more imformation.
为了避免异常终止,程序应在调用 hmean() 函数之前检查 x 和 y 的值。然而,依靠程序员来执行这种检查是不安全的。
返回错误码
一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。例如,ostream 类的 get(void) 成员通常返回下一个输入字符的 ASCII 码,但到达文件尾时,将返回特殊值 EOF。对 hmean() 来说,这种方法不管用。任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。在这种情况下,可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。istream 族重载 >> 运算符使用了这种技术的变体。通过告知调用程序是成功了还是失败了,使得程序可以采取除异常终止程序之外的其他措施。下面的程序是一个采用这种方式的示例,它将 hmean() 的返回值重新定义为 bool,让返回值指出成功了还是失败了,另外还给该函数增加了第三个参数,用于提供答案。
//error2.cpp -- returning an error code
#include<iostream>
#include<cfloat> // (or float.h) for DBL_MAX
bool hmean(double a, double b, double *ans);
int main(){
double x, y, z;
std::cout << "Enter two numbers: ";
while( std::cin >> x >> y){
if (hmean(x,y,&z)){
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
}
else{
std::cout << "One value should not be the negative "
<< "of the other - try again.\n";
}
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}
bool hmean(double a, double b, double *ans){
if (a == -b){
*ans = DBL_MAX;
return false;
}
else{
*ans = 2.0 * a * b / (a+b);
return true;
}
}
该程序的运行情况如下:
Enter two numbers: 4 6
Harmonic mean of 4 and 6 is 4.8
Enter next set of numbers <q to quit>: 7 -7
One value should not be the negative of the other - try again.
Enter next set of numbers <q to quit>: 1 19
Harmonic mean of 1 and 19 is 1.9
Enter next set of numbers <q to quit>: q
Bye!
程序说明
在上面的程序中,程序设计避免了错误输入导致的恶果,让用户能够继续输入。当然,设计确实依靠用户检查函数的返回值,这项工作是程序员所不经常做的。例如,为使程序短小精悍,本书的程序都没有检查cout是否成功地处理了输出。
第三参数可以是指针或引用。对内置类型的参数,很多程序员都倾向于使用指针,因为这样可以明显看出是哪个参数用于提供答案。
另一种在某个地方存储返回条件的方法是使用一个全局变量。可能问题的函数可以在出现问题时将该全局变量设置为特定的值,而调用程序可以检查该变量。传统的 C 语言数学库使用的就是这种方法,它使用的全局变量名为 errno。当然,必须确保其他函数没有将该全局变量用于其他目的。
异常机制
下面介绍如何使用异常机制来处理错误。C++异常是对程序运行过程中发生的异常情况(例如被0除)的一种响应。异常提供了将控制权从程序的一部分传递到另一部分的途径。对异常的处理有 3 个组成部分:
- 引发异常;
- 使用处理程序捕获异常;
- 使用 try 块。
程序在出现问题时将引发异常。例如,可以修改之前程序中的hmean(),使之引发异常,而不是调用 abort() 函数。throw语句实际上是跳转,即命令程序跳到另一条语句。throw 关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。
程序使用异常处理程序(exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。catch 关键字表示捕获异常。处理程序以关键字 catch 开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch 关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为 catch 块。
try 块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个 catch 块。try 块是由关键字 try 指示的,关键字 try 的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。
要了解这 3 个元素是如何协同工作的,最简单的方法是看一个简短的例子,如下面的程序所示:
// error3.cpp -- using an exception
#include<iostream>
double hmean(double a, double b);
int main(){
double x, y, z;
std::cout << "Enter two numbers: ";
while (std::cin >> x >> y){
try{ // start of try block
z = hmean(x,y);
} // end of try block
catch(const char * s){ // start of exception handler
std::cout << s << std::endl;
std::cout << "Enter a new pair of numbers: ";
continue;
} // end of handler
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}
double hmean(double a, double b){
if(a==-b){
throw "bad hmean() arguments: a = -b not allowed";
}
return 2.0 * a * b / (a+b);
}
该程序的运行情况如下:
Enter two numbers: 6 7
Harmonic mean of 6 and 7 is 6.46154
Enter next set of numbers <q to quit>: 10 -10
bad hmean() arguments: a = -b not allowed
Enter a new pair of numbers: 10 9
Harmonic mean of 10 and 9 is 9.47368
Enter next set of numbers <q to quit>: q
Bye!
程序说明
在上面的程序中,try 块与下面类似:
try{ // start of try block
z = hmean(x, y);
} // end of try block
如果其中的某条语句导致异常被引发,则后面的catch块将对异常进行处理。如果程序在 try 块的外面调用 hmean(),将无法处理异常
引发异常的代码与下面类似:
if ( a == -b)
throw "bad hmean() arguments: a = -b not allowed";
其中被引发的异常是字符串 “bad hmean() arguments: a = -b not allowed”。异常类型可以是字符串或其他C++类型;通常为类类型,本章后面的示例将说明这一点。
执行 throw 语句类似于执行返回语句,因为它也将终止函数的执行;但 throw 不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含 try 块的函数。在上面的程序中,该函数是调用函数。稍后将有一个沿函数调用序列后退多步的例子。另外,在这个例子中,throw 将程序控制权返回给 main()。程序将在 main() 中寻找与引发的异常类型匹配的异常处理程序(位于 try 块的后面)。
处理程序(或catch块)与下面类似:
catch (char * s){ // start of exception handler
std::cout << s << std::endl;
std::cout << "Enter a new pair of numbers: ";
continue;
} // end of handler
catch 块点类似于函数定义,但并不是函数定义。关键字 catch 表明这是一个处理程序,而 char* s 则表明该处理程序与字符串异常匹配。s 与函数参数定义极其类似,因为匹配的引发将被赋给 s。另外,当异常与该处理程序匹配时,程序将执行括号中的代码。
执行完 try 块中的语句后,如果没有引发任何异常,则程序跳过 try 块后面的 catch 块,直接执行处理程序后面的第一条语句。因此处理 3 和 6 时,下面程序中执行报告结果的输出语句。
接下来看将 10 和 -10 传递给 hmean() 函数后发生的情况。If 语句导致 hmean() 引发异常。这将终止 hmean() 的执行。程序向后搜索时发现,hmean() 函数是从 main() 中的 try 块中调用的,因此程序查找与异常类型匹配的 catch 块。程序中唯一的一个 catch 块的参数为 char*,因此它与引发异常匹配。程序将字符串 “bad hmean() arguments: a=-b not allowed” 赋给变量s,然后执行处理程序中的代码。处理程序首先打印 s——捕获的异常,然后打印要求用户输入新数据的指示,最后执行 continue 语句,命令程序跳过 while 循环的剩余部分,跳到起始位置。continue 使程序跳到循环的起始处,这表明处理程序语句是循环的一部分,而 catch 行是指引程序流程的标签。
您可能会问,如果函数引发了异常,而没有 try 块或没有匹配的处理程序时,将会发生什么情况。在默认情况下,程序最终将调用 abort() 函数,但可以修改这种行为。稍后将讨论这个问题。
将对象用作异常类型
通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch 块可以根据这些信息来决定采取什么样的措施。例如,下面是针对函数 hmean() 引发的异常而提供的一种设计:
#include<iostream>
class bad_hmean{
private:
double v1;
double v2;
public:
bad_hmean(int a = 0, int b = 0) : v1(a), v2(b) {}
void mesg();
};
inline void bad_hmean::mesg(){
std::cout << "hmean(" << v1 << ", " << v2 << "): "
<< "invalid arguments: a = -b\n";
}
class bad_gmean{
public:
double v1;
double v2;
bad_gmean(double a = 0, double b = 0) : v1(a), v2(b) {}
const char * mesg();
};
inline const char * bad_gmean::mesg(){
return "gmean() arguments should be >= 0\n";
}
// error4.cpp -- using exception classes
#include<iostream>
#include<cmath> // or math.h, unix users may need -lm flag
#include "15.10_exc_mean.h"
// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);
int main(){
using std::cout;
using std::cin;
using std::endl;
double x, y, z;
cout << "Enter two numbers: ";
while(cin >> x >> y){
try { // start of try block
z = hmean(x, y);
cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << endl;
cout << "Geometric mean of " << x << " and " << y
<< " is " << gmean(x, y) << endl;
cout << "Enter next set of numbers < q to quit>: ";
} // end of try block
catch(bad_hmean & bh){ // start of catch block
bh.mesg();
cout << "Try again.\n";
continue;
}
catch(bad_gmean & bg){
cout << bg.mesg();
cout << "Values used: " << bg.v1 << ", "
<< bg.v2 << endl;
cout << "Sorry, you don't get to play any more.\n";
break;
} // end of catch block
}
cout << "Bye!\n";
return 0;
}
double hmean(double a, double b){
if (a==-b){
throw bad_hmean(a,b);
}
return 2.0 * a * b / (a + b);
}
double gmean(double a, double b){
if (a < 0 || b < 0){
throw bad_gmean(a,b);
}
return std::sqrt(a * b);
}
下面是上述程序的运行情况,错误的 gmean() 函数输入导致程序终止:
Enter two numbers: 4 12
Harmonic mean of 4 and 12 is 6
Geometric mean of 4 and 12 is 6.9282
Enter next set of numbers < q to quit>: 5 -5
hmean(5, -5): invalid arguments: a = -b
Try again.
5 -2
Harmonic mean of 5 and -2 is -6.66667
gmean() arguments should be >= 0
Values used: 5, -2
Sorry, you don't get to play any more.
Bye!
首先,bad_hmean 异常处理程序使用了一条 continue 语句,而 bad_gmean 异常处理程序使用了一条 break 语句。因此,如果用户给函数 hmean() 提供的参数不正确,将导致程序跳过循环中余下的代码,进入下一次循环;而用户给函数 gmean() 提供的参数不正确时将结束循环。这演示了程序如何确定引发的异常(根据异常类型)并据此采取相应的措施。
其次,异常类 bad_gmean 和 bad_hmean 使用的技术不同,具体地说,bad_gmean 使用的是公有数据和一个公有方法,该方法返回一个 C-风格字符串。
异常规范和 C++11
有时候,一种理念看似有前途,但实际的使用效果并不好。一个这样的例子是异常规范(exception specification),这是 C++98 新增的一项功能,但 C++11 却将其摒弃了。这意味着 C++11 仍然处于标准之后,但以后可能会从标准中剔除,因此不建议您使用它。
然而,忽视异常规范前,您至少应该知道它是什么样的,如下所示:
double harm(double a) throw(bad_thing); // may throw bad_thing exception
double marm(double) throw(); // doesn't throw an exception
其中的 throw() 部分就是异常规范,它可能出现在函数原型和函数定义中,可包含类型列表,也可不包含。
异常规范的作用之一是,告诉用户可能需要使用 try 块。然而,这项工作也可使用注释轻松地完成。异常规范的另一个作用是,让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。这很难检查。例如,marm不会引发异常,但它可能调用一个函数,而这个函数调用的另一个函数引发了异常。另外,您给函数编写代码时它不会引发异常,但库更新后它却会引发异常。总之,编程社区(尤其是尽力编写安全代码的开发人员)达成的一致意见是,最好不要使用这项功能。而 C++11 也建议您忽略异常规范。
然而,C++11 确实支持一种特殊的异常规范:您可使用新增的关键字 noexcept 指出函数不会引发异常:
double marm() noexcept; // marm() doesn't throw an exception
有关这种异常规范是否必要和有用存在一些争议,有些人认为最好不要使用它(至少在大多数情况下如此);而有些人认为引入这个新关键字很有必要,理由是知道函数不会引发异常有助于编译器优化代码。通过使用这个关键字,编写函数的程序员相当于做出了承诺。
还有运算符 noexcept(),它判断其操作数是否会引发异常。