《C++ primer plus》第15章:友元、异常和其他(4)
异常
栈解退
假设 try 块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到 包含try块和处理程序的函数。这涉及到栈解退(unwinding the stack),下面进行介绍。
首先来看一看 C++ 通常是如何处理函数调用和返回的。C++通常通过将信息放在栈(参见第9章)中来处理函数调用。具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,依此类推。当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,依此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。
现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于 try 块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而 throw 语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将被不会被调用。
下面的函数是一个栈解退的示例。其中,main() 调用了 means(),而 means() 又调用了 hmean() 和 gmean()。函数 means() 计算算术平均数、调和平均数和几何平均数。main() 和 means() 都创建 demo 类型的对象(demo 是一个喋喋不休的类,指出什么时候构造函数和析构函数被调用),以便您知道发生异常时这些对象将被如何处理。函数 main() 中的 try 块能够捕获 bad_hmean 和 bad_gmean 异常,而函数 means() 中的 try 块只能捕获 bad_hmean 异常。catch 块的代码如下:
catch (bad_hmean & bh) { // start of catch blockbh.mesg();std::cout << "Caught in means()\n";throw; // rethrow the exception
}
上述代码显示消息后,重新引发异常,这将向上把异常发送给 main() 函数。一般而言,重新引发的异常将由下一个捕获这种异常的 try-catch 块组合进行处理,如果没有找到这样的处理程序,默认情况下程序将异常终止。下面的程序使用的头文件与之前一个相同(exec_mean.h)。
// error5.cpp -- unwinding the stack#include<iostream>
#include<cmath> // or math.h, unix users may need -lm flag
#include<string>#include"15.10_exc_mean.h"class Demo{
private:std::string word;
public:Demo(const std::string & str){word = str;std::cout << "Demo " << word << " created\n";}~Demo(){std::cout << "Demo " << word << " destroyed\n";}void show() const{std::cout << "Demo " << word << " lives!\n";}
};// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);
double means(double a, double b);int main(){using std::cout;using std::cin;using std::endl;double x, y, z;{Demo d1("found in block in main()");cout << "Enter two numbers: ";while(cin>>x>>y){try{ // start of try blockz = means(x, y);cout << "The mean mean of " << x << " and " << y<< " is " << z << endl;} // end of try blockcatch (bad_hmean & bh){ // start of catch blockbh.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}d1.show();}cout << "Bye!\n";cin.get();cin.get();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);
}double means(double a, double b){double am, hm, gm;Demo d2("found in means()");am = (a+b)/2.0; // arithmetic meantry{hm = hmean(a,b);gm = gmean(a,b);}catch (bad_hmean & bh){bh.mesg();std::cout << "Caught in means()\n";throw; // rethrow the exception}d2.show();return (am+hm+gm)/3.0;
}
下面是上述程序的运行情况:
Demo found in block in main() created
Enter two numbers: 6 12
Demo found in means() created
Demo found in means() lives!
Demo found in means() destroyed
The mean mean of 6 and 12 is 8.49509
6 -6
Demo found in means() created
hmean(6, -6): invalid arguments: a = -b
Caught in means()
Demo found in means() destroyed
hmean(6, -6): invalid arguments: a = -b
Try again.
6 -8
Demo found in means() created
Demo found in means() destroyed
gmean() arguments should be >= 0
Values used: 6, -8
Sorry, you don't get to play any more.
Demo found in block in main() lives!
Demo found in block in main() destroyed
Bye!
程序说明
来看看该程序的运行过程。首先,正如 Demo 类的构造函数指出的,在 main() 函数中创建了一个 Demo 对象。接下来,调用了函数 means(),它创建了另一个 Demo 对象。函数 means() 使用 6 和 2 来调用函数 hmean() 和 gmean(),它们将结果返回给 means(),后者计算一个结果并将其返回。返回结果前,means() 调用了 d2.show();返回结果后,函数 means() 执行完毕,因此自动为 d2 调用析构函数:
demo found in means() lives!
demo found in means() destroyed
接下来的输入循环将值6和-6发送给函数means(),然后 means() 创建一个新的 demo 对象,并将值传递给 hmean()。函数 hmean() 引发 bad_hmean 异常,该异常被 means() 中的 catch 块捕获,下面的输出指出了这一点:
hmean(6, -6) : invalid arguments : a = -b
Caught in means()
该 catch 块中的 throw 语句导致函数 means() 终止执行,并将异常传递给 main() 函数。语句 d2.show() 没有被执行表明 means() 函数被提前终止。但需要指出的是,还是为 d2 调用了析构函数:
Demo found in means() destroyed
这演示了异常极其重要的一点:程序进行栈解退以回到能够捕获异常的地方时,将释放栈中的自动存储型变量。如果变量是类对象,将为该对象调用析构函数。
与此同时,重新引发的异常被传递给 main(),在该函数中,合适的 catch 块将捕获它并对其进行处理:
hmean(6, -6) : invalid arguments: a = -b
Try again.
接下来开始了第三次输入循环:6 和 -8 被发送给函数 means()。同样,means() 创建一个新的 Demo 对象,然后 6 和 -8 传递给 hmean(),后者在处理它们时没有出现问题。然而,means() 将 6 和 -8 传递给 gmean(),后者引发了 bad_gmean 异常。由于 means() 不能捕获 bad_gmean 异常,因此异常被传递给main(),同时不再执行 means() 中的其他代码。同样,当程序进行栈解退时,将释放局部的动态变量,因此为 d2 调用了析构函数:
Demo found in means() destroyed
最后,main() 中的 bad_gmean 异常处理程序捕获了异常,循环结束:
gmean() arguments should be >= 0
Values used: 6, -8
Sorry, you don't get to play any more.
然后程序正常终止:显示一些消息并自动 d1 调用析构函数。如果 catch 块使用的是 exit(EXIT_FAILURE) 而不是 break,则程序将立刻终止,用户将看不到下述消息:
Demo found in main() lives!
Bye!
但仍能够看到如下消息:
Demo found in main() destroyed
同样,异常机制将负责释放栈中的自动变量。
其他异常特性
虽然 throw-catch 机制类似于函数参数和函数返回机制,但还是有些不同之处。其中之一是函数 fun() 中的返回语句将控制权返回到调用 fun() 函数,但 throw 语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的 try-catch 组合。例如,在上面的程序中,当函数 hmeans() 引发异常时,控制权将传递给函数 means();然而,当 gmeans() 引发异常时,控制权将向上传递到 main()。
另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使异常规范和 match 块中指定的是引用。例如,请看下面的代码:
class problem { ... };
...
void super() throw (problem) {...if (oh_no){problem oops; // construct objectthrow oops; // throw it...}
}
...
try {super();
}
catch(problem & p){// statements
}
p 将指向 oops 的副本而不是 oops 本身。这是件好事,因为函数 super() 执行完毕后,oops 将不复存在。顺便说一句,将引发异常和创建对象组合在一起将更简单:
throw problem(); // construct and throw default problem object
您可能会问,既然 throw 语句将生成副本,为何代码中使用引用呢?毕竟,将引用作为返回值的通常原因是避免创建副本以提高效率。答案是,引用还有另一个重要特征:基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。
假设有一个异常类层次结构,并要分别处理不同的异常类型,则使用基类引用将能够捕获任何异常对象;而是用派生类对象只能捕获它所属类及从这个类派生而来的类的对象。引发的异常对象将被第一个与之匹配的 catch 块捕获。这意味着 catch 块的排列顺序应该与派生顺序相反:
class bad_1 {...};
class bad_2 : public bad_1 {...};
class bad_3 : public bad_2 {...};
...
void duper(){...if (oh_no)throw bad_1();if (rats)throw bad_2();if (drat)throw bad_3();
}
...
try {duper();
}
catch(bad_3 & be){// statements
}
catch(bad_2 & be){// statements
}
catch(bad_1 & be){// statements
}
如果将 bad_1 & 处理程序放在最前面,它将捕获异常 bad_1、bad_2、bad_3;通过按相反的顺序排列,bad_3 异常将被 bad_3 & 处理程序所捕获。
提示:如果有一个异常类继承层次结构,应这样排列 catch 块:将捕获位于层次结构最下面的异常类的 catch 语句放在最前面,将捕获基类异常的 catch 语句放在最后面。
通过正确地排列 catch 块的顺序,让您能够在如何处理异常方面有选择的余地。然而,有时候可能不知道会发生哪些异常。例如,假设您编写了一个调用另一个函数的函数,而您并不知道被调用的函数可能引发哪些异常。在这种情况下,仍能够捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常:
catch(...) { // catches any type exception// statements
}
如果知道一些可能会引发的异常,可以将上述捕获所有异常的 catch 块放在最后面,这有点类似于 switch 语句中的 default:
try {duper();
}
catch (bad_3 & be){// statements
}
catch (bad_2 & be) {// statements
}
catch (bad_1 & be) {// statements
}
catch (bad_hmean & h) {// statements
}
catch(...) { // catch whatever is left// statements
}
可以创建捕获对象而不是引用的处理程序。在 catch 语句中使用基类对象时,将捕获所有的派生类对象,但派生特性将被剥去,因此将使用虚方法的基类版本。