异常

2018-12-31  本文已影响0人  gpfworld
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派生自定义异常类有
        一个非常大的好处就是,异常类的继承结构很规范和统一,如果
        子程序中定义了非常多的异常基类的话,程序异常类型的管理很
        混乱,不利于使用一个基类统一捕获所有派生类异常。
上一篇 下一篇

猜你喜欢

热点阅读