C和C++的异常处理
异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。而传统错误处理技术,检查到一个局部无法处理的问题时。
C的异常处理:
1、 终止程序(除数为0)。
2、 返回一个表示错误的值(很多系统函数都是这样,例如malloc,内存不足,分配失败,返回NULL指针)。
3、 返回一个表示错误的值,附加错误码(GetLastError())
4、 调用一个预先准备好在出现"错误"的情况下用的函数。
5、 暴力解决:如abort()或者exit()。
6、 使用goto语句,不幸的是,goto是本地的:它只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所有代码都在main体中)。
7、 Set jmp()和long jmp():
(1)setjmp(j)设置"jump"点,用正确的程序上下文填充jmp_buf对象j,当初始化完jump的上下文,setjmp()返回0值。
(2)以后调用longjmp(j,r)的效果就是一个非局部的goto或"长跳转"到由j描述的上下文处(也就是到那原来设置j的setjmp()处)。当作为长跳转的目标而被调用时,setjmp()返回r或1(如果r设为0的话)。(记住,setjmp()不能在这种情况时返回0。)
通过有两类返回值,setjmp()让你知道它正在被怎么使用。当设置j时,setjmp()如你期望地执行;但当作为长跳转的目标时,setjmp()就从外面"唤醒"它的上下文。你可以用longjmp()来终止异常,用setjmp()标记相应的异常处理程序。
注意:
(1)set jump必须先调用,在异常位置通过调用long jmp以恢复先前被保存的程序执行点,否则将导致不可预测的结果,甚至程序崩溃。
(2)在调用setjump的函数返回之前调用longjmp,否则结果不可预料。
缺陷:函数的使用者必须非常靠近函数调用的地方编写错误处理代码,无疑使代码变得臃肿笨拙。
(3)set jmp()和long jmp并不能很好的支持C++面向对象的语意。
For example:
#include<iostream>
#include<windows.h>
usingnamespacestd;
#include<string>
#include<setjmp.h>
jmp_bufbuf;//set_jmp属于C函数库,是一个结构体类型,作用是分别承担非局部标号和goto作用
voidFuntest1()
{
longjmp(buf,1);
//longjmp(buf,0);//注意:不能这么写,容易与setjmp默认返回值0混淆,但两者不是同一个0;
}
voidFuntest2()
{
longjmp(buf,2);
}
intmain()
{
intstate =setjmp(buf);
if (0 ==state)
{
Funtest1();
Funtest2();
}
else
{
switch (state)
{
case 1:
cout << "Funtest1 ()" <<endl;
break;
case 2:
cout << "Funtest2 ()" <<endl;
break;
}
}
system("pause");
return 0;
}
C++的异常处理:
异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,传统的错误处理是用不同的数值表示不同类型的错误,其表达能力有限,因为一个数字包含的信息量太少,而C++异常处理机制将异常类型化,显然一个类型要比一个数字包含的信息量大得多。
一、异常处理的语法结构:
组成部分:抛出异常、提炼异常、捕获异常以及异常对象本身。
try { 复合语句 } catch ( 异常类型 [名称]) { 复合语句 } catch ( 异常类型 [名称] ) { 复合语句 } 。
for example1:
doubleDiv(intleft,intright)
{
if (right == 0)
{
throw -1; //提前检测异常发生条件,并抛出自定义异常
}
returnleft /right; //这才是真正可能刚发生错误的地方
}
//因此,当异常抛出时,正真错误并没发生。
intmain()
{
try
{
Div(5, 0);
}
catch (inta)//catch语句是根据throw抛出的 - 1的类型来捕捉的 ,若catch (int char),则没捕捉到抛出的异常,因为类型不匹配,所以异常将一直被抛向上层调用者,直至系统(系统的处理是很暴力的哦,直接结束程序)
{
cout <<"除数为0" <<endl;
}
system("pause");
return 0;
}
for example2:
void funtest0()
{
FILE *fp;
try
{
fp= fopen("tst","rb");//试图打开一个不存在的文件
if (NULL == fp)
{
throw -1; //抛出异常;
}
}
catch (int err)
{
cout<< "err" << endl;
}
fclose(fp);//打开文件之后要关闭,否则其他用户无法访问
}
二、throw抛出异常:
1、throw在行为上更像是一个goto语句。
2、throw语句位于当前try内(直接或间接,直接就是throw value在try内,间接就是 throw value位于所在函数,而所在函数位于try内)。
3、如果一个异常在抛出点没有得到处理,那么它将一直被抛向上层调用者,直至main()函数,知道找到一个类型匹配的异常处理器,否则系统则(调用terminate())中断程序。若找到catch子句并处理以后,会继续沿着catch子句后面继续执行。
4、抛出的异常类型可以是任何类型:如基本的内置类型(如:int,float ,char....),也可以是用户自定义的一些异常类来具体描述我们需要的异常类型(如:struct,enum,class......)。
三、异常的类型匹配规则;
c++规定:当一个异常对象和catch字句的参数类型符合下列条件时,匹配成功。
1、如果catch子句参数类型就是异常对象的类型或其引用。
2、如果catch子句的参数类型是异常对象所属类型的Public基类或其引用。
3、如果catch子句的参数类型为public基类指针,而异常对象为派生类指针。
4、catch子句参数类型为void*,而异常对象为任何类型指针。
5、catch子句为catch-all,即catch(...)。
6、允许从非const对象到const对象的转换。
7、允许派生类到基类的转换。
8、允许从数组到数组类型的指针的转换。
9、允许函数转换为指向函数类型的指针。
class Base
{};
classDerive:public Base
{};
voidfuntest1()
{
int*p = (int*)malloc(0x7fffffff);
if (NULL ==p)
assert(p);
free(p);
}
DeriveD;
voidfuntest2()
{
throwD;
}
voidfuntest3()
{
inta = 1;
throw 1;
}
voidfuntest4()
{
intarr[2];
throwarr;
}
voidfun(){cout <<"fun()" <<endl; }
voidfuntest5()
{
throwfun;
}
Derive *D1;
voidfuntest6()
{
throwD1;
}
intmain()
{
try
{
funtest6();
funtest5();
funtest4();
funtest3();
funtest2();
funtest1();
}
catch (Base *b)//允许派生类指针到基类指针的转换
{
cout <<"Base *D" <<endl;
}
catch (BaseD)//允许派生类到基类的转换
{
cout <<"D" <<endl;
}
catch (constinterr)//允许从非const对象到const对象的转换
{
cout <<"const->err" <<endl;
}
catch (int*arr_err)//允许从数组到数组类型的指针的转换
{
cout <<"arr_err" <<endl;
}
catch (void(*p)())//允许函数转换为指向函数类型的指针
{
p();// 运行结果fun();
}
catch (interro)//允许函数转换为指向函数类型的指针
{
cout <<"erro" <<endl;
}
system("pause");
return 0;
}
四、异常重抛(异常转换):
异常重抛(rethrow),是在catch块中使用一个 空 throw 语句来达到此目的,重抛后程序立刻退出当前 try/catch 范围而进入上一层范围,有可能是上一层调用者,也有可能是嵌套的try/catch结构的上一级。 也可以在 catch 块内抛出一个不同于当前异常类型的异常对象,这样可以实现异常转换, 并让上层调用者来进一步处理。
五、函数异常说明;
异常说明是为了加强程序的可读性。
Double Devide(double x , double y) throw(DevidedByZero); //假设DevidedByZero是一个类类型(1)表示抛出一种异常
Bool F(const char *)throw(T1, T2, T3); //(2)表示可能有三种异常
void g()throw;//(3)不抛出任何异常
void k(); //(4)可能抛出任何异常,也可能不抛出任何异常,这会使调用者不知所措
六、异常&构造函数&析构函数:
1、构造函数为什么不能抛出异常。
(1)构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或者没有完成初始化。
(2)构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。
(3)因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。
(4)构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。(如何保证?使用auto_ptr?)
2、析构函数为什么不能抛出异常?
(1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
(2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
(3)那么当无法保证在析构函数中不发生异常时,该怎么办?
其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。
这是一种非常简单,也非常有效的方法。
~base()
{
try{
do_something();
}
catch()
{ //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。
}
}
注意;
1、异常抛出点常常和异常捕获点距离很远,异常抛出点可能深埋在底层软件模块内,而异常捕获点常常在高层组件中;异常捕获必须和try块(异常提炼)组合使用,
并且可以通过异常组合在一个地点捕获多种异常。
2、在一个函数内尽量不要出现多个并列的try块,也不要使用嵌套的try块,否则不仅会导致程序结构复杂化,增加运行时的开销,而且容易出现逻辑错误。
3、对于每一个被抛出的异常,总能找出对应的throw语句,只是有些是位于我们的程序之中,有些是位于标准库之中,这就是我们常常能catch到一个异常却看不到它放在哪里的原因。
4、虽然异常对象看上去像局部对象,但是它并非创建在函数的堆栈上,而是创建在专用的异常堆栈上,因此它才可以跨越多个函数传递到上层,否则创建在堆栈清退的过程中就会被销毁。
5、不要企图把局部对象的地址作为异常对象抛出,因为局部对象会在异常抛出后函数堆栈清退的过程中被销毁。