异常:程序执行期间,可检测到的不正常情况。
例如:0作除数;数组下标越界;打开不存在的文件;远程机器连接超时;malloc失败等等。
程序的两种状态:
正常状态和异常状态,发生不正常情况后,进入异常状态,从当前函数开始,按调用链的相反次序,查找处理该异常的程序片断。
1.throw 表达式
语义:用表达式的值生成一个对象(异常对象),程序进入异常状态。
Terminate函数,终止程序的执行。
2.try-catch语句
try{
包含可能抛出异常的语句;
}catch(类型名 [形参名]){ //类型名和throw抛出的数据类型对应
}catch(类型名 [形参名]){
}
例子程序:
#include <iostream>
#include <math.h>
using namespace std;
double sqrt_delta(double d){
if(d < 0)
throw 1;
return sqrt(d);
}
double delta(double a, double b, double c){
double d = b * b - 4 * a * c;
return sqrt_delta(d);
}
void main()
{
double a, b, c;
cout << "please input a, b, c" << endl;
cin >> a >> b >> c;
while(true){
try{
double d = delta(a, b, c);
cout << "x1: " << (d - b) / (2 * a);
cout << endl;
cout << "x2: " << -(b + d) / (2 * a);
cout << endl;
break;
}catch(int){
cout << "delta < 0, please reenter a, b, c.";
cin >> a >> b >> c;
}
}
}
3.重新抛出异常
语法: throw;
语义:重新抛出原有的异常对象。如果在throw后面有表达式,则抛出新的异常对象。
例子程序:
#include <iostream>
using namespace std;
void fun(int x){
try{
if(x == 1)
throw 1;
if(x == 2)
throw 1.0;
if(x == 3)
throw ''1'';
}catch(int){
cout << "catch an int in fun()" << endl;
}catch(double){
cout << "catch an double in fun()" << endl;
}
cout << "testing exception in fun()..."<< endl;
}
void gun()
{
try{
//fun(1);
//fun(2);
//fun(3);
fun(4);
}catch(char){
cout << "catch a char in gun()" << endl;
}
cout << "testing exception in gun()..."<< endl;
}
int main()
{
gun();
}
4.扑获所有异常
catch(...){
}
下面的程序是不对的:
error C2311: ''int'' : is caught by ''...'' on line 7
#include <iostream>
using namespace std;
void fun()
{
try{
}catch(...){
cout << "catch all exception ..." << endl;
}catch(int){
cout << "catch int exception ..." << endl;
}
}
5.异常规范
指出函数可以抛出的所有异常类型名。
语法:
返回值类型 函数名(形参表) throw(类型名表) {
函数体
}
异常规范跟随在函数参数表之后,用关键字throw来声明,后面括号内的是异常类型表。
若不加异常规范,表示函数可能抛出任何类型的异常;若异常类型表为空,表示函数保证不抛出任何异常;若函数要抛出多个异常,则在类型表中用逗号隔开。
空异常规范表示不抛出异常;
例如:
warning C4297: ''function'' : function assumed not to throw an exception but does
__declspec(nothrow) or throw() was specified on the function
#include <iostream>
using namespace std;
void function(int x) throw() //指定的类型名是int
{
if(x == 1)
throw 1;
}
无异常规范表示可抛出任何异常。
异常规范违例,在函数的声明中并没有声明抛出该类异常,但在程序中却抛出了该类的异常?例如:
warning C4290: C++ exception specification ignored except to indicate a function is not __declspec(nothrow)
void function(int x) throw (int) //函数的声明
{
if(x == 1)
throw 1.5;
}
注:在g++中并未警告。
对于函数指针,例如:
#include <iostream>
using namespace std;
void function(int x) throw(int)
{
if(x == 1)
throw 1;
}
int main()
{
void (*fp)(int)throw(char);
fp = function;
fp(1);
}
同样的,在g++中没有警告,但在vc8中提出警告:
warning C4290: C++ exception specification ignored except to indicate a function is not __declspec(nothrow) pan>
<补充>
异常规范违例,例子程序如下:
#include <iostream>
using namespace std;
class A
{
};
void function(int x)throw(int)
//void function(int x)throw(A*)
{
if(x == 1)
throw new A;
}
void test() throw (A* )
{
//void (*fp)(int)throw(A);
void (*fp)(int)throw(int);
fp = function;
try{
fp(1);
}catch(int)
{
cout << "test" << endl;
throw;
}
}
int main()
{
try{
test();
}catch(A*){
cout << "test in main" <<endl;
}
return 0;
}
这个代码在vc8和g++环境中的运行结果不同?
<补充>
1.异常处理仅仅通过类型而不是通过值来匹配的,否则又回到了传统的错误处理技术上去了,所以catch块的参数可以没有参数名称,只需要参数类型,除非要使用那个参数。
2.虽然异常对象看上去像局部对象,但并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。
3.函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。由于异常处理机制是在运行时有异常时才发挥作用的,因此如果函数的实现中抛出了没有在其异常说明列表中列出的异常,则编译器并不能检查出来。但是当运行时如果真的抛出了这样的异常,就会导致异常冲突。因为你没有提示函数的调用者:该函数会抛出一种没有被说明的即不期望的异常,于是异常处理机制就会检测到这个冲突并调用标准库函数unexcepted(),unexcepted()的默认行为就是调用terminate()来结束程序。
实际工作中使用set_unexcepter()来预设一个回调函数。
4.当异常抛出时局部对象如何释放?
Bjarne Stroustrup引入了“resource acquistion is initialization”思想,异常处理机制保证:所有从try到throw语句之间构造起来的局部对象的析构函数将被自动调用,然后清退堆栈(就像函数正常退出一样)。如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。
5.catch块的参数应采用引用传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。catch(void *)要放到catch(...)前面。
6.编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。
二、为什么要选用异常处理的编程方法?
当然更为重要的是,C++中引入的异常处理的编程机制提供给程序员一种全新的、更好的编程方法和思想。在C++中明确提出trycatch异常处理编程方法的框架之前的年代,程序员是怎样编写程序的,如下:
void main(int argc, char* argv[])
{
if (Call_Func1(in, param out)
{
// 函数调用成功,我们正常的处理
if (Call_Func2(in, param out)
{
// 函数调用成功,我们正常的处理
while(condition)
{
//do other job
if (has error)
{
// 函数调用失败,表明程序执行过程中出现一些错误,
// 因此必须处理错误
process_error();
exit();
}
//do other job
}
}
else
{
// 函数调用失败,表明程序执行过程中出现一些错误,
// 因此必须处理错误
process_error();
exit();
}
}
else
{
// 函数调用失败,同样是错误处理
process_error();
exit();
}
}
因为程序的执行过程中总会遇到许多可预知或不可预知的错误事件,例如说,由于内存资源有限导致需要分配的内存失败了;或某个目录下本应存在的一个文件找不着了;或说不小心被零除了、内存越界了、数组越界了等等。这些错误事件存在非常大的隐患,因此程序员总需要在程序中不断加入if语句,来判断是否有异常出现,如果有,就必须要及时处理,否则可能带来意想不到的,甚至是灾难性的后果。这样一来,程序可读性差了很多,总是有许多与真正工作无关的代码,而且也给程序员增加了极大的工作负担,多数类似的处理错误的代码模块就像满山的牛屎一样遍地都是(程序员不大多是“牛”人吗?所以。。。哈哈)。
但C++中的异常处理的机制彻底改变了这种面貌,它使真正的计算处理和错误处理分开来,让程序员不再被这些琐碎的事情所烦扰,能关注于真正的计算处理工作。同时代码的可读性也好了。因此我们有理由选择异常处理的编程方法。具体原因如下:
1、 把错误处理和真正的工作分开来;
2、 代码更易组织,更清晰,复杂的工作任务更容易实现;
3、 毫无疑问,更安全了,不至于由于一些小的疏忽而使程序意外崩溃了;
4、 由于C++中的try catch可以分层嵌套,所以它提供了一种方法使得程序的控制流可以安全的跳转到上层(或者上上层)的错误处理模块中去。(不同于return语句,异常处理的控制流是可以安全地跨越一个或多个函数 )。
5、 还有一个重要的原因就是,由于目前需要开发的软件产品总是变得越来越复杂、越来越庞大,如果系统中没有一个可靠的异常处理模型,那必定是一件十分糟糕的局面。
相信绝大多数程序员都知道C++中的异常处理的编程方法,可还是有很多人已习惯原来单纯的面向过程的代码组织方式,不太习惯或较少使用trycatch异常处理。为了使您编写的代码更安全;为了使您编写的代码让他人更易阅读,主人公阿愚强烈建议在您书写的代码中尽可能多用异常处理机制,少一些不必要的if判断语句。
异常的组织
1.将异常组织成层次树状结构,使用继承来实现层次树状结构。这样做的好处是(1) 可以帮助异常的处理,(2)对异常可以进行扩展。
2.在C++中,当异常被抛出时被复制,所以处理器catch中得到的只是原始异常的一个副本,所以要求定义的异常类是可复制的。
异常的处理
1.在catch中进行异常的处理时,最好使用指针或者引用来避免C++中,上溯造型时带来的信息丢失问题。
2.重新抛出异常,形式是throw;
3.捕捉所有的异常使用,形式是catch(...)
4.捕捉异常的顺序是从具体到抽象
关键字
1、 try
2、 catch
3、 throw
其中关键字try表示定义一个受到监控、受到保护的程序代码块;关键字catch与try遥相呼应,定义当try block(受监控的程序块)出现异常时,错误处理的程序模块,并且每个catch block都带一个参数(类似于函数定义时的数那样),这个参数的数据类型用于异常对象的数据类型进行匹配;而throw则是检测到一个异常错误发生后向外抛出一个异常事件,通知对应的catch程序块执行对应的错误处理。
语法
1、还是给一个例子吧!如下:
int main()
{
cout << "In main." << endl;
//定义一个try block,它是用一对花括号{}所括起来的块作用域的代码块
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是int,值为1)
//由于在try block中的代码是受到监控保护的,所以抛出异常后,程序的
//控制流便转到随后的catch block中
throw 1;
cout << "在 try block 中, 由于前面抛出了一个异常,因此这里的代码是不会得以执行到的" << endl;
}
//这里必须相对应地,至少定义一个catch block,同样它也是用花括号括起来的
catch( int& value )
{
cout << "在 catch block 中, 处理异常错误。异常对象value的值为:"<< value << endl;
}
cout << "Back in main. Execution resumes here." << endl;
return 0;
}
2、语法很简单吧!的确如此。另外一个try block可以有多个对应的catch block,可为什么要多个catch block呢?这是因为每个catch block匹配一种类型的异常错误对象的处理,多个catch block呢就可以针对不同的异常错误类型分别处理。毕竟异常错误也是分级别的呀!有致命的、有一般的、有警告的,甚至还有的只是事件通知。例子如下:
int main()
{
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
return 0;
}
3、一个函数中可以有多个trycatch结构块,例子如下:
int main()
{
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
//这里是二个trycatch结构块,当然也可以有第三、第四个,甚至更多
try
{
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
return 0;
}
4、上面提到一个try block可以有多个对应的catch block,这样便于不同的异常错误分类处理,其实这只是异常错误分类处理的方法之一(暂且把它叫做横向展开的吧!)。另外还有一种就是纵向的,也即是分层的、trycatch块是可以嵌套的,当在低层的trycatch结构块中不能匹配到相同类型的catch block时,它就会到上层的trycatch块中去寻找匹配到正确的catch block异常处理模块。例程如下:
int main()
{
try
{
//这里是嵌套的trycatch结构块
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
return 0;
}
5、讲到是trycatch块是可以嵌套分层的,并且通过异常对象的数据类型来进行匹配,以找到正确的catch block异常错误处理代码。这里就不得不详细叙述一下通过异常对象的数据类型来进行匹配找到正确的catch block的过程。
(1) 首先在抛出异常的trycatch块中查找catch block,按顺序先是与第一个catch block块匹配,如果抛出的异常对象的数据类型与catch block中传入的异常对象的临时变量(就是catch语句后面参数)的数据类型完全相同,或是它的子类型对象,则匹配成功,进入到catch block中执行;否则到二步;
(2) 如果有二个或更多的catch block,则继续查找匹配第二个、第三个,乃至最后一个catch block,如匹配成功,则进入到对应的catch block中执行;否则到三步;
(3) 返回到上一级的trycatch块中,按规则继续查找对应的catch block。如果找到,进入到对应的catch block中执行;否则到四步;
(4) 再到上上级的trycatch块中,如此不断递归,直到匹配到顶级的trycatch块中的最后一个catch block,如果找到,进入到对应的catch block中执行;否则程序将会执行terminate()退出。
另外分层嵌套的trycatch块是可以跨越函数作用域的,例程如下:
void Func() throw()
{
//这里实际上也是嵌套在里层的trycatch结构块
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
//由于这个trycatch块中不能找到匹配的catch block,所以
//它会继续查找到调用这个函数的上层函数的trycatch块。
throw 1;
}
catch( float& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
}
int main()
{
try
{
Func();
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
catch( int& value )
{
//这个例子中,Func()函数中抛出的异常会在此被处理
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
return 0;
}
6、刚才提到,嵌套的trycatch块是可以跨越函数作用域的,其实这里面还有另外一层涵义,就是抛出异常对象的函数中并不一定必须存在trycatch块,它可以是调用这个函数的上层函数中存在trycatch块,这样这个函数的代码也同样是受保护、受监控的代码;当然即便是上层调用函数不存在trycatch块,也只是不能找到处理这类异常对象错误处理的catch block而已,例程如下:
void Func() throw()
{
//这里实际上也是嵌套在里层的trycatch结构块
//由于这个函数中是没有trycatch块的,所以它会查找到调用这个函数的上
//层函数的trycatch块中。
throw 1;
}
int main()
{
try
{
//调用函数,注意这个函数里面抛出一个异常对象
Func();
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
catch( int& value )
{
//这个例子中,Func()函数中抛出的异常会在此被处理
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
//如果这里调用这个函数,那么由于main()已经是调用栈的顶层函数,因此不能找
//到对应的catch block,所以程序会执行terminate()退出。
Func();
// [特别提示]:在C++标准中规定,可以在程序任何地方throw一个异常对象,
// 并不要求一定只能是在受到try block监控保护的作用域中才能抛出异常,但
// 如果在程序中出现了抛出的找不到对应catch block的异常对象时,C++标
// 准中规定要求系统必须执行terminate()来终止程序。
// 因此这个例程是可以编译通过的,但运行时却会异常终止。这往往给软件
// 系统带来了不安全性。与此形成对比的是java中提供的异常处理模型却是不
// 永许出现这样的找不到对应catch block的异常对象,它在编译时就给出错误
// 提示,所以java中提供的异常处理模型往往比C++要更完善,后面的章节
// 会进一步对这两种异常处理模型进行一个详细的分析比较。
return 0;
}