我觉得看到这篇文章的人肯定有很多疑问,比如:什么是异常?为什么要使用异常?使用异常会对程序代码有什么影响以及不使用又会怎样?等等。那么接下来我一一为你解答,跟着我揭开异常的面纱。
【我们还是先来了解一下什么是异常?】
1、了解异常之前,我先问大家,我们写出的程序,如果它消亡了,会有几种消亡的形式?
我总结了一下,大体上,一共有三种:
1)无疾而终:就是程序自然的消亡,就是它的周期到了,就像我们人类,到了一定的年龄,就会老去。
2)自杀:这个指的是程序中处理的时候会有不可能的情况,也就是说如果出现这种情况的话,我们不做处理,程序可能就会崩溃啊等等,那么这个时候我们自己就将它做些处理,将这种情况给它干掉,不等系统啊,编译器啊,为我们做处理。说了这么些,有点蒙圈吧,我举个例子,比如我们在写一个函数(随意),如果里面你传进去的参数是个指针,而正好这个函数里如果你传入的是一个空指针,程序就会崩溃,那么这个时候我们就可以自己加一个if语句将这种情况处理掉
3)他杀:顾名思义,自己不做主动处理,比如你桌面上打开了一个页面,然后关闭的时候从应用管理器那里进行关闭
2、我们先来看C语言里是如何处理异常的?
传统的处理异常的几种方式:
1)程序终止
int Div(int a,int b)
{
if(b == 0)
{
exit(1);
}
return (a/b);
}
2)返回一个错误的值,附加错误码(
GetLastError())
GetLastError返回的值通过在api函数中调用SetLastError或SetLastErrorEx设置。函数并无必要设置上一次错误信息,所以即使一次GetLastError调用返回的是零值,也不能担保函数已成功执行。只有在函数调用返回一个错误结果时,这个函数指出的错误结果才是有效的。通常,只有在函数返回一个错误结果,而且已知函数会设置GetLastError变量的前提下,才应访问GetLastError;这时能保证获得有效的结果。SetLastError函数主要在对api函数进行模拟的dll函数中使用。
GetLastError的返回值的含义有很多。我在这里列举几个:
(0)-操作成功完成。
(1)-功能错误。
(2)- 系统找不到指定的文件。
(3)-系统找不到指定的路径。
(4)-系统无法打开文件。
(5)-拒绝访问。
3)返回一个合法值,让程序处于某种非法状态(很坑的atoi())
如果在一个重要系统中,调用者不知情,传入了一个NULL字符,程序就异常退出了,导致服务中断,或者传入非法字符,结果返回0,代码继续走下去,在复杂的系统中想要定位这个问题,真是很不容易。
4)调用一个预先准备好在出现"错误"的情况下用的函数。
这个是什么意思呢:就是说你把自己的错误处理函数已经处理好了,写在一个函数中了,当你的错误出现的时候,就可以去调用它,一般这个错误处理函数都是通过函数指针传递进去的,在出现错误的时候直接把这个错误处理函数调用下。
5)暴力解决方式:abort()或者exit()
a、前者只是终止程序,不会做清理工作
b、后者会做一些清理工作,比如:
6)使用goto语句:只能在函数的内部跳转
7)setjmp()和longjmp()组合:可以在不同函数间进行跳转
我来详细讲解下这两个函数的使用:
他不像它不像abort
或者assert
或者exit
那样直接退出,也不像goto
语句仅仅局限在函数内部。
它是用于一种长跳转的方式。可以从一个函数跳到这个函数上层的调用函数中。
a、函数 A 中调用了setjmp
设置了一个跳转位,然后函数A调用了函数B。
b、函数 B 中调用了longjmp
,那么会使得程序条到 函数 A中调用setjmp
的位置继续执行。
直接上代码吧:
#include<iostream>
using namespace std;
#include<setjmp.h>
#include<stdlib.h>
jmp_buf mark;
void FunTest1()
{
longjmp(mark, 1);
}
void FunTest2()
{
longjmp(mark, 2);
}
void FunTest3()
{
longjmp(mark, 3);
}
int main()
{
int iState = setjmp(mark);
if (0 == iState)
{
FunTest1();
FunTest2();
FunTest3();
}
else
{
switch(iState)
{
case 1:
cout<< "FunTest1() Error"<<endl;
break;
case 2:
cout<< "FunTest2() Error"<<endl;
break;
case 3:
cout<< "FunTest3() Error"<<endl;
break;
}
}
cout<< "Main End"<<endl;
return 0;
}
关于函数setjmp和函数longjmp的使用方法:
1)首先用setjmp设置跳转的地点,setjmp的参数buf是用来保存设置跳转点时的函数使用的重要数据,当从其他函数跳转回来,如果不用这个保存的数据恢复当前函数的一些数据的话,跳转回来是不能运行的。第一次设置的时候setjmp返回值为0
2)首先用setjmp设置跳转的地点,setjmp的参数buf是用来保存设置跳转点时的函数使用的重要数据,当从其他函数跳转回来,如果不用这个保存的数据恢复当前函数的一些数据的话,跳转回来是不能运行的。第一次设置的时候setjmp返回值为0
在这里举得这个例子里,执行main函数里的第一条语句后,不执行下面的if语句,跳转到longjmp(mark,1),执行完后,跳转到case语句里。执行case1,所以打印出来的是 FunTest1 Error.
这两个函数使用要注意的点:
1)setjump必须先调用,在异常位置通过调用longjmp以恢复先前被保存的程序执行点,否则将导致
不可预测的结果,甚至程序崩溃。
2)在调用setjmp的函数返回之前调动longjmp,否则结果不可预料。
使用这两个函数的缺陷:
1)函数的使用者必须非常靠近函数调用的地方编写错误处理代码,无疑使代码变的臃肿笨拙。
2)setjmp()和longjmp()并不能够很好的支持C++面向对象的语义。
3、c++的异常处理:当一个函数发现自己无法处理的错误时抛出异常,让函数的调用者直接或间接的处理这个问题。
使用异常,就把错误和处理分开来,由库函数抛出异常,
由调用者捕获这个异常,调用者就可以
知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
使用异常,就把错误和处理分开来,由库函数抛出异常,
由调用者捕获这个异常,调用者就可以
知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
使用异常,就把错误和处理分开来,由库函数抛出异常,
由调用者捕获这个异常,调用者就可以
知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
使用异常,就把错误和处理分开来,由库函数抛出异常,
由调用者捕获这个异常,调用者就可以
知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。我们要关注的点就是三个:try,throw,catch
1)异常的抛出与捕获
.异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。
. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
. 抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的异常对象副本(匿名对象),异常对象由编译管理,异常对象在传给对应的catch处理之后撤
我们来举个例子说明下,怎么使用。
a、捕获的第一种方式
#include<iostream>
using namespace std;
void Funtest()
{
FILE* fp = fopen("1.text","r");
if(fp == NULL)
{
throw 1; //有可能出现的异常的地方抛出异常
}
fclose(fp);
}
int main()
{
try //在这个代码块里放入你你有可能出现异常的函数
{
Funtest();
}
catch(int err) //按照抛出异常的类型来捕获,可以捕获多次,但是不能是同一种类型的
{
cout<<"catch(int err)"<<endl;
}
return 0;
}
b、捕获的第二种方式:重新定义一个函数,将有异常的函数放入此函数的中,并在此函数中进行捕获,如果不在,则在主函数中也进行捕获,如果还不在,那么将不会进入主函数中,如果异常流到系统中则会崩溃
c、捕获位置异常:格式就是catch()的括号里放“...”就可以了
#include<iostream>
using namespace std;
void Funtest()
{
FILE* fp = fopen("1.text","r");
if(fp == NULL)
{
throw 1;
}
fclose(fp);
}
int main()
{
try
{
Funtest();
}
catch(int err)
{
cout<<"catch(int err)"<<endl;
}
catch(...)
{
cout<<"未知异常"<<endl;
}
return 0;
}
2)异常捕获的匹配规则异常对象的类型与catch说明符的类型必须完全匹配。只有以下几种情况例外:
a. 允许从非const对象到const的转换。
b. 允许从派生类型到基类类型的转换。
c. 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的指针
3)异常的重新抛出有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。(即就是嵌层调用)
4)异常规范
在函数声明之后, 列出该函数可能抛出异常类型, 并保证该函数不会抛出其他类型的异常。
(1)、 成员函数在类内声明和类外定义两处必须有相同的异常规范。
(2)、 函数抛出一个没有被列在它异常规范中的异常时(且函数中抛出异常没有在函数内部进行处理),
系统调用C++标准库中定义的函数unexpected().
(3)、 如果异常规范为throw(),则表示不得抛出任何异常, 该函数不用放在try块中。
(4)、 派生类的虚函数的异常规范必须与基类虚函数的异常规范一样或更严格(是基类虚函数的异常
的子集)。 因为: 派生类的虚函数被指向基类类型的指针调用时, 保证不会违背基类成员函数的异常
规范。
5)异常与构造函数&析构函数
a. 构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
b. 析构函数主要完成资源的清理,需要保证不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
class B
{
public:
B()
{
p = new int[10];
throw 1;
}//如果抛出异常的话可能会没有释放掉P,造成内存泄漏
~B()
{} //同理
}
【我们了解了异常,肯定还是还有疑问,为什么要使用异常,它虽然存在,不使用可以吗?】
1、异常分离了接收和处理错误代码可以使用if判断 ,但是当你的循环,分支多重嵌套的时候, 就会出现代码可读性差, 逻辑混乱 ,用异常可以绝大程度避免这个问题
2、异常是用来简化错误处理的.
因为异常有类型,所以可以方便的进行分层处理.而且异常可以携带更丰富的信息,方便错误的定位处理.异常可以无视正常的程序流程,在有复杂调用层次关系的时候,这可以极大简化错误处理,而不用一层一层的退出
关于404异常,请听下回分析!