1. 异常与c语言的判错处理
几乎所有的高级语言都有异常处理,只是c语言是一个例外,c语言只有简单的错误处理。
在c++中引入了异常处理机制,该机制被很多面向对象的语言继承和借用,在java中的异
常处理与c++几乎是一样的,在java中异常处理使用的更加频繁。
c++中的可以抛出任意类型的异常,这些类型可以是基本类型,自定义类类型,系统提供
的特定异常类类型。但是在java中有些不同,在java中只能抛出系统提供的异常,就算是
抛出自定义类类型的异常,该类型也必须是继承于系统提供的异常基类。
严格来说,在c语言中是没有真正意义上的异常处理的,c++中异常处理与c的出错处理
对比起来,c++的出异常处理最大的优势在于,它将正常代码,可能出错的代码以及异
常处理的代码进行有效的分隔。
但是在c中不是这样的,在c中,正常代码与出错处理的代码混在了一起,使得程序的有
些混乱。做过较大c语言项目的人会发现,在c项目中出错处理的代码往往比正常代码多
很多,而且都混杂在一起,非常不利于c代码的阅读理解。
2.为什么学习异常
对于程序员来说,不光是要考虑程序正常执行时会得到什么结果,更多的是要考虑在程
序运行的过程中,会产生哪些异常,针对这些异常必须提前做好异常出现的防范措施。
比如用户从终端输入了错误数据,就应该抛出异常,在这些异常中,我们应该给出重新
输入正确数据的提示。如果检测出网络断开了,或者网络速度非常的慢,那么我们就应
该抛出网速异常,根据异常提示,用户可以选择退出或者选择继续等待。
在c++中往往是c方式的判错与c++异常处理方式并用的,甚至c判错方式比c++异常的方式
使用的更多,但是较大型的c++开发程序中,在很多关键的地方我们需要使用异常而不是
使用简单的判错。
异常很重要,但是对于初学c++的程序员来说,感受可能并不深刻,异常使用的频繁都甚
至高于类模板,因为类模板主要用于构建c++程序框架时用的,涉及程序架构的问题,
我们经常要做的就是使用别人定义好的类别模板框架,比如容器就是一个典型的例子。
3. 异常处理方式
检测到异常后,针对异常大致就这几种处理方式:
(1)使用abort()函数终止整个程序,c++默认的异常处理就是这样的,在c语言中,针
对错误处理,我们经常也是使用abort()函数或者exit()函数直接异常或者正常
终止进程。
(2)忽略异常
(3)纠正异常错误,使程序正常的运行。
异常产生后,具体采用那种方式,就看程序员在写代码的过程中,根据客户的业务逻辑需
求确定合适的异常处理。
4.异常处理的模型
(1)常用关键字
try:try块用于包含可能会产生异常的代码
throw:抛出/转抛异常
catch:catch块包含用于处理异常的代码,一个try块往往会跟很多catch块,因为可能
会有很多不同类型的异常发生。
(2)异常处理结构
正常代码
try {
可能产生异常的代码
if(异常) throw 异常;
}
catch(异常情况1) {
异常处理
}
catch(异常情况...){
异常处理
}
catch(异常情况n) {
异常处理
}
正常代码
(3)异常处理的过程
(1)产生异常并抛出异常
(2)异常匹配
(3)处理异常
(4)处理异常结束
5.具体的异常操作
(1)抛出异常与异常匹配
在大多数时候,对于基本类型往往直接使用c语言判错方式进行处理,在c++中也可
以使用异常进行处理。
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
int main(void)
{
int a = 10;
try{
//检测到异常后抛出异常
if(a > 2) throw "数值超出范围";
c = a/b;
}
catch (const char *ex) {
cout<<ex<<endl;
}
printf("程序继续执行\n");
return 0;
}
运行结果:
数值超出范围
例子分析:
try块中,如果a>2会主动throw抛出异常,抛出的异常类型是字符串常量,因
此会到catch块进行异常匹配,将抛出的异常类型拿去与catch块中形参的类型
匹配,如果匹配成功,将异常传递给异常形参,并执行异常代码,这里的异常
代码很简单,只是将异常信息进行简单的打印。
例子中需要注意的地方:
(1)在异常的类型匹配中,const会参与类型匹配
在例子中如果将catch形参中的const去掉,异常将不会被匹配成功,
因为抛出的时字符串常量,需要常量类型的形参接收。
(2)处理异常时,如果决定终止程序,可以调用exit()正常终止进程,不要
调用abort()函数,这回导致异常终止,会带来一些不确定因素。
(2)异常处理的详细过程
(1)抛出异常,抛出时会建立一个临时的异常对象
(2)将抛出的临时异常对象的类型拿去与catch块的形参类型进行匹配
(3)如果匹配成功,临时的异常对象将会初始化catch的形参,如果是类类型的对象,
这个过程会调用副本构造函数(拷贝构造函数)进行初始化。
(4)将临时对象释放,如果临时对象是类类型对象,类的析构函数将会被调用
(5)将程序控制权转移到异常处理代码中,进行异常处理
(6)异常处理结束,程序要么被终止或则继续执行异常后面的正常代码
6.未被处理的异常
c++程序中产生的异常分为两类,
(1)第一类:由os产生,并发送信号给程序,如果是在linux这边,我们可以自己实现
对信号的处理。
(2)第二类:自己抛出的异常
针对第一类os产生的异常,如果我们不希望由系统产生异常,我们应该在程序中
及时的提前判断出异常,然后自己抛出异常并按照自己定义的方式去处理异常。
c++的异常处理基本处理的都是自己抛出的异常。
(1)当系统自动产生的异常未被处理是时
实际上在os产生异常时,程序会被发送一个相关的信号。在讲liunx系统编程时,我们知道,
当程序进行非法操作时,系统会产生异常,os会给程序发送一个信号,这些信号的默认处理
方式基本都是将程序异常终止。比如下面产生/0异常的例子。
对/0进行做除法,linux会向c++程序直接发送一个SIGFPE的算数异常信号,这个信号的默认
处理方式就是异常终止进程。
例子:
#include <iostream>
#include <stdio.h>
#include <string>
#include <errno.h>
using namespace std;
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
cout<<"除法运算";
c = a/b;
cout<<"继续执行"<<endl;
return 0;
}
运行结果:
浮点数例外
例子分析:
程序被linux内核发送一个SIGFPE算数异常信号,该信号直接将进程异常终止,
正是由于是异常终止的,我们发现"除法运算"这句话一直被积压在缓冲区中而
没有被打印出来。
根据在linux系统编程的知识,我们可以:
(1)使用默认处理方式对待该信号,显然该信号的默认处理方式时异常终止进程。
(2)忽略:大多数信号都是可以被忽略的,但是我们会发现这个信号无法被忽
略,因为算数很严重,不允许忽略,就算是我们将其设置为忽略,系统
也会按照默认处理方式异常终止进程。
(3)捕获该信号,在信号处理函数中进行自定义处理,显然是可以。
比如针对以上C++代码,我们可以对SIGFPE信号进行捕获。
上例改进后:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <signal.h>
#include <errno.h>
#include <exception>
using namespace std;
void signal_fun(int signo) {
cout<<"信号处理函数,signo= " <<signo<<endl;
exit(-1);
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
cout<<"除法运算";
signal(SIGFPE, signal_fun);
//signal(SIGFPE, SIG_IGN);
c = a/b;
return 0;
}
运行结果:
信号处理函数,signo=8
例子分析:
对信号进行了捕获,信号处理函数会被调用,将信号编号将会被打印出来后,
紧接着调用exit结束,因为exit是正常终止,因此积压在缓冲区的内容会被
刷新出来。
如果将//signal(SIGFPE, SIG_IGN);打开,对信号进行忽略,同样你会发现该
信号无法被忽略。
如果我们将信号处理函数中的exit函数去掉,会导致循环触发异常和调用信号
处理函数。
(2)当c++代码自己抛出的异常未被catch匹配到
(1)未能匹配到异常的情况
正常情况下,我们会避免由系统发信号告诉程序异常发生,因为在这种方式下,
很多时候,信号的处理方式都是默认方式处理的,默认处理方式这都会导致一些
不确定后果,我们应该避免这种情况存在。
因此在c++中,需要在执行可能会产生异常的代码之前,先进行相应的if判断,
如果判断发现异常情况后,c++代码应该主动抛出异常,至于如何对待异常,其
控制权完全掌握在程序员的手中。
到目前为止,我们已经学会了如何主动抛出异常,但是如果没有异常处理代码处
理异常,或者在catch块中没有匹配上该异常,出现这种情况时,程序会调用c++
库提供的ternimate函数,该函数将会调用abort函数将进程异常终止,由于程序
是被abort()函数异常终止的,因此会导致很多不确定状态的发生。
将上面的例子进行修改成为刚才描述的情况:
#include <iostream>
#include <stdio.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try{
if(0 == b) throw " 异常,除数为0";
c = a/b;
}
catch (int ex) {
cout<<ex<<endl;
}
catch (char *ex) {
cout<<ex<<endl;
}
printf("程序继续执行\n");
return 0;
}
运行结果:
terminate called after throwing an instance of 'char const*'
已放弃
例子分析:
之所以导致上面的运行结果,是因为throw抛出的异常在catch块中无法匹配到,
因此调用了c++库提供的terminate函数,直接将进程终止了。由于直接被终止,
导致printf("程序继续执行\n");这句话不能被执行。
(2)重写ternimate函数
为了防止terminate函数调用abort函异常终止程序而导致的不确定性,我们往往
会自定义一个terminate函数,然后将该函数进行注册即可生效,至于自定的函数
的名称可以使任意的。
操作步骤:
(1)自定义ternimate函数
自定义格式为:void (* 函数名)() { }
格式要求无返回值/无参数,但是函数名可以是任意的,函数名代表
的只是一个函数地址,注册时,注册的只是函数地址,注册后,函数
地址会被赋值给terminate函数指针变量。
注册后,当产生未能匹配的异常时,自定义的ternimate函数就会被
回调,当然我们自己也可以在程序中主动的调用ternimate()函数,只
是这么做的意义并不是很大。
比如:
void myternimate() {
内容
}
(2)注册
注册时,注册函数为:
terminate_hanfler pold_exhandler = set_terminate(newhandler);
terminate_hanfler是typedef后的类型,真实类型为:void (*)(),
其实就是自定义terminate函数时要求的类型。
之所以返回值也是这种类型,是因为调用注册函数进行注册时,会返回
旧的terminate函数的地址,通过返回的这个旧的处理函数地址就可以还
原旧的terminate函数。
实际上,我们完全可以在不同的程序阶段,按照不同的要求注册不同的
terminate函数。
(3)举例
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
void new_terminate() {
cout << "新的terminate函数" << endl;
exit(1);//正常结束
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
terminate_handler old_terminate = NULL;
old_terminate = set_terminate(new_terminate);
try{
if(0 == b) throw " 异常,除数为0";
c = a/b;
}
catch (int ex) {
cout<<ex<<endl;
}
catch (char *ex) {
cout<<ex<<endl;
}
printf("程序继续执行\n");
return 0;
}
运行结果:
新的terminate函数
例子分析:
自定义了terminate函数,然后将其进行注册,当程序出现无法匹配的
异常时,就会自动调用注册的terminate函数,当然我们也可以在程序
中主动的调用terminate或者new_terminate函数,事实上,在自定义的
terminate函数中,我们还可以做很多的事情。
7. 系统异常与c++代码主动抛出的异常区别
系统抛出的异常基本都是由os抛出的(这里默认c++程序运行在os上),系统执了会产
生异常的代码后,系统会发送信号给进程,表示异常发生。
但是系统抛出的异常往往会导致一些不希望的结果,虽然可以对信号进行捕获,在信号
处理函数里面进行一些自定义的操作,但是我们不提倡这么做,因为这非常不利于提高
程序的可移植性,比如,我们根本无法确定linux这边信号处理方式与windows提供的处
理机制是否相同。
为了避免由系统产生异常,我们往往会提前判断是否会发生异常,如果是就throw主动抛
出异常,根据自定义的异常处理方式进行处理。
比如想前面除0的例子一样,我们不应该让os去检测除0的异常,而是我们自己提前判断
出除0的异常,然后按照自己的方式去处理这个异常。
不过c++异常的抛出与java中的异常抛出有些不一样,java中异常可以throw手动抛出,
也可以自动抛出,针对自动抛出的异常,不需要进行if异常的判断,自动抛出后会自动到
catch进行异常的匹配处理,在java/安卓中,很多的异常几乎都是自动抛出,很少进行
if判断,然后throw手动抛出。
在c++中不一样,抛出的异常前,需要使用if进行异常判断,判断出有异常后,需要使用
throw手动的抛出异常。
9.嵌套的try块
try可以相互嵌套,内层try块的异常先由内层处理,如果匹配不上再由外层try块处
理,外层try块的异常只能由外层处理。
异常处理可以嵌套任意深度,但是嵌套太深没有意义,具体嵌套深度应该由程序实际
需求决定。
从前面的学习中我们知道,try块中包含的都是哪些可能会导致异常的代码,但这并不意
味着必须将所有可能会导致异常代码都放在try块中。
如果在try块中包含有子函数,那么子函数也是处于try块中。只是当try块中包含有子函
数时,那么子函数中可能有异常处理,也可能没有异常处理,如果有异常处理的话,实
际上这也是异常处理的嵌套,与直接进行异常嵌套效果是一次样的。
(1)当函数中没有有异常处理,或者没有匹配上异常处理时
直接将异常返回一级的异常处理代码处理,如果还没有处理,继续向上返回异常。
(2)如果函数中有异常处理
(1)如果匹配成功直接处理
(1)如果在异常中有调用exit函数,进程会被直接终止
(2)否者一次返回调用函数的最上一层
例子1:直接嵌套
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try{
try{
if(0 == b) throw "异常,除数为为0";
c = a/b;
}
catch(int ex) {
cout <<"内层异常处理"<<ex<<endl;
}
}
catch (int ex) {
cout<<ex<<endl;
}
catch (const char *ex) {
cout<<"外层处理"<<ex<<endl;
}
printf("程序继续执行\n");
return 0;
}
例子2:嵌套函数中的异常处理
(1)当try中的子函数没有包含异常处理时的例子:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
int division1(int a, int b) {
if(0 == b) throw "异常,除数为0";
return a/b;
}
int division(int a, int b)
{
division1(a, b);
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try{
c = division(a, b);
}
catch (int ex) {
cout<<ex<<endl;
}
catch (char *ex) {
cout<<ex<<endl;
}
printf("程序继续执行\n");
return 0;
}
例子分析:
try块中调用了division子函数,division函数又调用了division1子函数,
所以这两个子函数都是try块的一部分,division1中的throw抛出异常后,
由于divison1和division函数都没有异常处理的代码,因此该异常会留给
最上一级main函数中的异常处理代码进行处理。
(2)当try中的子函数有包含异常处理时的例子:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
int division1(int a, int b) {
try{
if(0 == b) throw "异常,除数为0";
return a/b;
}
catch (const char *ex) {
cout << ex << endl;
}
}
int division(int a, int b)
{
division1(a, b);
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try{
c = division(a, b);
}
catch (int ex) {
cout<<ex<<endl;
}
catch (const char *ex) {
cout<<"外层处理"<<ex<<endl;
}
printf("程序继续执行\n");
return 0;
}
10. 对类对象异常处理
(1)自定义异常类
c++中,可以对基本类型进行异常处理,在前面的例子中已经使用过,c++中还可以对
任意类型的对象进行异常处理。
根据需要总是定义特殊的类,用于表示特殊的异常情况,该类总是包含异常消息或者
用于处理异常的代码。
有了自定义异常类,还可以以此为基类派生出其它自定义异常子类。
例子:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <typeinfo>
#include <errno.h>
using namespace std;
class FPException {
public:
FPException(const string exmsg="异常"):exmsg(exmsg) {}
string get_exmsg() const { return exmsg; }
private:
string exmsg;
};
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try{
if(0 == b) throw FPException("算数异常,除数为0");
c = a / b;
}
catch (int ex) {
cout<<ex<<endl;
}
catch (const FPException ex) {
cout<<ex.get_exmsg()<<endl;
}
printf("程序继续执行\n");
return 0;
}
(2)有关catch块中异常匹配的问题
(1)catch形参类型与抛出的异常类型之间的类型匹配
(1)基本类型类型
要求类异常型完全一致时才能匹配上,const会参与匹配。
(2)类类型
进行类类型异常的匹配时,会涉及自动类型的转换,如果出现以下情况时,
异常都会匹配成功,但const不会参与匹配。
(1)形参类型与抛出的异常类型完全相同,匹配成功,const不参与匹配。
(2)形参类型是抛出异常类型的基类(直接或者间接),匹配成功,
const不参与匹配。
(3)形参类型是抛出异常类型的基类(直接或者间接)的引用,匹配成功,
const不参与匹配。
(4)形参和异常是指针,异常可以自动转换形参类型,匹配成功,
同样const不参与匹配。
(2)catch块的匹配顺序
总是从第一个catch块开始依次向后进行匹配,但是由于类类型匹配时存在自动
类型转换的问题,因此要求对类类型的catch进行排序,将最特殊的派生类异常
放到最前面,将最一般的基类异常放到后面,否者异常匹配时,由于自动类型
转换的原因,永远只匹配基类异常,派生类异常永远得不到匹配。
例子:
#include <iostream>
using namespace std;
/* 异常基类 */
class Exception {
public:
Exception(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
/* 算数异常类 */
class FPException : public Exception {
public:
FPException(const string exmsg="算术异常"):Exception(exmsg) { }
};
/* 除零算数异常 */
class DividZeroFPException : public FPException {
public:
DividZeroFPException(const string exmsg="除零算术异常"):FPException(exmsg) { }
};
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
for(int i=0; i<8; i++) {
try {
if(1 == i) throw Exception();
if(3 == i) throw FPException();
if(7 == i) throw DividZeroFPException();
}
catch (DividZeroFPException &ex) {
cout<<ex.get_exmsg()<<endl;
}
catch (const FPException ex) {
cout<<ex.get_exmsg()<<endl;
}
catch (const Exception ex) {
cout<<ex.get_exmsg()<<endl;
}
}
return 0;
}
运行结果:
异常
算术异常
除零算术异常
(3)使用基类匹配所有的异常
如果最前面catch块捕获的异常是基类异常,那么后面派生类异常的catch块永远
没有被匹配的机会。
如果不希望在异常处理中出现太多异常处理分支,那么我们完全可以只用一个基
类异常处理完所有基类的派生类异常类型,但是这有一个问题,那就是不利于进
行异常的分类处理,处理太过于笼统,因此需要根据实际情况找好平衡点。
将上面的例子修改如下:
例子:
#include <iostream>
using namespace std;
/* 异常基类 */
class Exception {
public:
Exception(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
/* 算数异常类 */
class FPException : public Exception {
public:
FPException(const string exmsg="算术异常"):Exception(exmsg) { }
};
/* 除零算数异常 */
class DividZeroFPException : public FPException {
public:
DividZeroFPException(const string exmsg="除零算术异常"):FPException(exmsg) { }
};
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
for(int i=0; i<8; i++) {
try {
if(1 == i) throw Exception();
if(3 == i) throw FPException();
if(7 == i) throw DividZeroFPException();
}
catch (const Exception ex) {
cout<<ex.get_exmsg()<<endl;
}
catch (const FPException ex) {
cout<<ex.get_exmsg()<<endl;
}
catch (DividZeroFPException &ex) {
cout<<ex.get_exmsg()<<endl;
}
}
return 0;
}
例子分析:
在catch中,将基类异常放在了前面,派生类异常放在了后面,那么派生类异
常将永远无法执行,因为会被基类基类异常拦截,在这种情况,完全可以将
派生类去掉,只留下基类异常。
11.捕获所有异常
前面提到过,如果catch块无法匹配上时,会调用c++库提供的terminate函数处理异常,
但是有些时候我们希望能够捕获所有的异常,就像switch语句的default一样,如果所
有的选择都无法匹配上的话,最后的default一定会处理。
对于catch快,使用省略号作为参数,表示能够处理所有各种类型的异常:
catch(...) {
处理异常的代码
}
例子:
#include <iostream>
using namespace std;
/* 基类异常 */
class Exception {
public:
Exception(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try {
if(0 == b) throw Exception("除0异常");
c = a / b;
}
catch (int ex) {
cout<<ex<<endl;
}
catch (float ex) {
cout<<ex<<endl;
}
catch (...) {
cout<<"产生异常"<<endl;
}
return 0;
}
运行结果:
产生异常
例子分析:
抛出的异常将会被catch (...) 捕获。
12. 重新抛出异常
(1)内层嵌套异常处理中重新抛出异常
在catch块中捕获到异常时,可以重新抛出异常,抛出的异常由外层异常块处理。
转抛的方式就是使用throw关键字实现,转抛的过程不是复制异常的过程,只是转抛。
例子:
#include <iostream>
using namespace std;
/* 基类异常 */
class Exception {
public:
Exception(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try {
try {
if(0 == b) throw Exception("除0异常");
c = a / b;
}
catch (Exception ex) {
cout << "内层异常\n" << endl;
throw ex;//重新抛出异常
}
}
catch (const Exception ex) {
cout << "外层异常" << endl;
cout<<ex.get_exmsg()<<endl;
}
return 0;
}
运行结果:
内层异常
外层异常
除0异常
例子分析:
内层捕获到异常后,并没有自己处理该异常,而是使用throw将异常转抛给外层
的异常处理。
(2)在嵌套的函数异常处理中抛出或者重新抛出异常
(1)重新抛出异常举例
例子:
#include <iostream>
using namespace std;
/* 基类异常 */
class Exception {
public:
Exception(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
int division(int a, int b)
{
try {
if(0 == b) throw Exception("除0异常");
return a/b;
}
catch (Exception &ex) {
cout << "内层异常\n" << endl;
throw ex;//重新抛出异常
}
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try {
division(a, b);
}
catch (const Exception &ex) {
cout << "外层异常" << endl;
cout<<ex.get_exmsg()<<endl;
}
return 0;
}
运行结果:
内层异常
外层异常
除0异常
例子分析:
本例子实际上与前面例子效果一致。
(2)限制函数向上重抛的异常
函数向上一层函数返回异常的情况:
(1)在子函数的异常处理代码中虽然匹配到了,但是在catch块中通过throw转抛后,
会被抛给上一层函数
(2)在子函数中没有异常处理,或者没有匹配到异常处理时,异常会被抛给上一层
但是我们可以限制子函数向上一层函数抛出异常。
限制的格式,比如:
int fun(int a) 限制说明 {
内容
}
异常限制说明的3种情况:
(1)没有异常说明:表示可以向上一层抛出任何类型异常
(2)throw():不能向上抛出任何类型的异常
(3)throw(异常类型列表),比如throw(E1, E2)
可以抛出括号中指定类型的异常
(1)对于基本类型来说
向上抛出的异常的类型,必须与指定的允许抛出的异常列表中
指定的类型完全相同时,才能抛给上一层。
(2)对类类型来说
向上抛出的异常类型,只要是允许的异常列表中指定类型的派
生类型即可。
(3)限制抛出异常举例子
例子1:不允许向上抛出异常
#include <iostream>
int division(int a, int b) throw()
{
try {
//if(0 == b) throw DividZeroFPException("除0异常");
if(0 == b) throw 1;
return a/b;
}
catch (float ex) {
cout << "内层异常\n" << endl;
throw ex;//重新抛出异常
}
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try {
division(a, b);
}
catch (int ex) {
cout << "外层异常" << endl;
}
return 0;
}
运行结果:
terminate called after throwing an instance of 'int'
已放弃
例子分析:
在division函数中,由于异常不能匹配成功,所以会将异常向上抛给上一层
函数,但是由于异常抛出的限制声明为throw(),表示不可以向上抛出任何异
常,所有导致向上无法抛出异常,在这种情况下,程序只能自动的调用c++库
提供的terminate函数处理异常。
例子2:抛出类类型
#include <iostream>
using namespace std;
/* 异常基类 */
class Exception1 {
public:
Exception1(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
/* 异常基类 */
class Exception2 {
public:
Exception2(const string exmsg="异常"):exmsg(exmsg) { }
string get_exmsg() const { return exmsg; }
protected:
string exmsg;
};
/* 算数异常类 */
class FPException : public Exception1 {
public:
FPException(const string exmsg="算术异常"):Exception1(exmsg) { }
};
int division(int a, int b) throw(Exception1, Exception2) {
try {
//if(0 == b) throw DividZeroFPException("除0异常");
if(0 == b) throw FPException("算术异常,除数为0");
return a/b;
}
catch (FPException &ex) {
cout << "内层异常处理\n" << endl;
throw ex;//重新抛出异常
}
}
int main(void)
{
int a = 10;
int b = 0;
int c = 0;
try {
division(a, b);
}
catch (Exception1 &ex) {
cout << "外层异常处理" << endl;
cout<<ex.get_exmsg()<<endl;
}
return 0;
}
运行结果:
内层异常处理
外层异常处理
算术异常,除数为0
例子分析:
在division函数中,将FPException向上重新抛出,但是限制说明只允许向上
抛出Exception1和Exception2,貌似不允许抛出FPException异常,但是由于
该异常是Exception1的派生类,所以实际上是允许向上抛出的该类型异常的。
(4)未预料到的异常
略
(5)构造函数中的异常处理
略
(6)析构函数中的异常处理
略
13. 标准库提供的异常类
(1)c++提供的标准异常类
前面提到,在c++中允许抛出任何类型的异常,这些异常可以是基本类型的异常,自定义
类类型的异常,也可以是c++库提供的标准异常。
在前面也描述过,这一点与java很不一样,在java中只能使用标准库提供的异常,就算
是使用自定义的异常,也必须是继承自标准异常类后实现的自定义异常。
c++之所以提供标准异常类,主要是为了标准化异常操作,当然我们完全可以使用自己
自定义异常类来代替,exception异常属于std::命名空间,使用标准异常类时需要包含
<stdexecpt>头文件。
基于exceptin类的派生关系如下:
exception基类
|
|------bad_cast:类型转换时可能会抛出的异常,比如dynamic_cast<>()就抛出
|
|------bad_typeid:使用typeid()时可能会抛出的异常
|
|------bad_exception:“匹配意外性”异常,与在catch中指定...功能同
|
|------bad_alloc:new开辟空间时可能会抛出的异常
|
|------ios_base::failure(内部类):进行io操作可能会抛出的异常
|
|------logic_error:可以提前检查出来的异常(编程时可以提前想到异常)
| |
| |-------length_error:字符串长度超过string要求的长度时导致的异常
| |
| |-------out_of_range:数组字符串index索引越位异常
| |
| |-------invalid_argument:无效参数异常
|
|
|------runtime_error:运行时异常,运行期间才能检查到的异常
|
|-------overflow_error:上溢出异常,比如浮点数正向溢出
|
|-------underflow_error:下溢异常,比如浮点数反向溢出
|
|-------range_error:计算结果不再允许范围内的异常
(2)exception基类
exception是c++中所有的异常类的原始基类,该异常类的大致定义结构为:
class exception {
public:
exception() throw;
eception (const exception&) throw();
exception& operator=(const exception&) throw();
virtual ~exception() throw();
virtual const char * what() const throw();//函数返回描述异常情况的非空字符串
};
从上面这个类型中我们发现,所有的函数后面都加了throw()声明,表示函数不允许向上
一层抛出异常。
我们发现what函数是virtual,也就意味着后面所有的派生异常类的what函数都是virtual
的,这么一来就,通过多态的方式,调用基类的what接口就可以实现派生类waht函数的调用。
try {
}
catch (exception &ex) {
}
通过使用exception这一个基类便可以捕获所有的该异常类的派生类异常,但是缺点是没有
实现异常的细分,不利于对异常进行精确处理。
如果程序中抛出了非exception异常类或其派生类的话,使用...便可捕获所有可能的异常
但是缺点任然还是无法实现异常的精确分类和处理。
(3)使用标准库
(1)使用标准异常库有两种方式
(1)可以在自己的程序中直接使用标准异常类
(2)可以从标准异常类派生出自己的异常类
不管是直接使用还是派生,在程序中经常使用的标准异常类大都
是logic_error和runtime_error。
(2)直接标准异常类例子
(1)使用logic_error异常
#include <iostream>
#include <stdexcept>
using namespace std;
int main(void)
{
int a = 20;
int b = -1;
int buf[5];
int index = 7;
try {
if(index > 5) throw out_of_range("数组访问越位异常");
buf[7] = 10;
}
catch (out_of_range &ex) {
cout<<ex.what()<<endl;
}
return 0;
}
(2)使用runtime_error
#include <iostream>
#include <stdexcept>
using namespace std;
void test_fun(int a, int b) throw() {
try {
if(a<0 || b>10) throw range_error("范围异常");
}
catch(range_error &ex) {
cout<<ex.what()<<endl;
}
}
int main(void)
{
int a = 0;
int b = 0;
cout<<"请输入整数"<<endl;
cin>>a;
cout<<"请输入整数"<<endl;
cin>>b;
test_fun(a, b);
return 0;
}
(3)从标准异常基类中派生出自己的异常类
#include <iostream>
#include <stdexcept>
using namespace std;
class my_range_error : public exception {
public:
my_range_error(const char *const msg): msg(msg) { }
virtual ~my_range_error()throw() { }
virtual const char* what() const throw() { return msg;}
private:
const char *msg;
};
void test_fun(int a, int b) throw() {
try {
if(a<0 || b>10) throw my_range_error("范围异常");
}
catch(my_range_error &ex) {
cout<<ex.what()<<endl;
}
}
int main(void)
{
int a = 0;
int b = 0;
cout<<"请输入整数"<<endl;
cin>>a;
cout<<"请输入整数"<<endl;
cin>>b;
test_fun(a, b);
return 0;
}
运行结果:
请输入整数
-1
请输入整数
2
范围异常
例子分析:
看了这个例子后,有些同学可能会疑惑,这种方式跟自定义一个
异常类有什么区别呢,还不如自定义一个呢。
表面上看,确实是这样的但是从exception派生自定义异常类有
一个非常大的好处就是,异常类的继承结构很规范和统一,如果
子程序中定义了非常多的异常基类的话,程序异常类型的管理很
混乱,不利于使用一个基类统一捕获所有派生类异常。
网友评论