关于面向对象设计的重要特性--多态性到上一节纯虚函数和抽象类就讲完了。这一讲是本C++编程入门教程的最后一讲--异常处理。

       我们开发的软件一般按照正常的流程操作时运行不会出问题,但是用户不一定会根据软件工程师的想法来操作软件,而且往往随机性很大,另外,软件的运行环境也会改变,例如硬盘空间不足、文件被移走,这些都可能会导致软件出现异常,甚至崩溃。所以我们进行软件开发时要充分考虑异常的捕捉和处理。

       一.异常处理的基本思想

       进行异常处理的目标是,使软件具有容错能力,在出现运行环境或者异常操作等问题时,程序能够继续往下运行,必要时弹出提示信息。

       软件开发中往往每个函数都有自己的分工,很多出现错误的函数都不会处理错误,而是产生一个异常让调用者捕捉和处理。如果调用者也不能处理此异常,则异常就会被继续向上级调用者传递,这个传递过程会一直持续到异常能被处理为止。如果程序最终没能处理这个异常,那么它就会被传递给C++的库函数terminate,然后terminate会调用abort函数终止程序。

        二.C++异常处理的语法

        异常处理机制是靠try、throw和catch语句实现的。

        throw语句的形式为:

        throw 表达式

        try块的语法形式为:

        try
        {
                  复合语句
        }
        catch(异常类型的声明)
        {
                  复合语句
        }
        catch(异常类型的声明)
        {
                  复合语句
        }
        ...

       先说说throw语句,当某段程序有了不能处理的异常时,就可以用“throw 表达式”的方式传递这个异常给调用者。这里throw后的表达式在语法上与return后的表达式类似。

       再来看try块的try子句,子句后括号里的复合语句就是被监测的程序段。如果某段程序或者调用的某个函数可能会产生异常,就把它放到try后。当try子句后的程序段发生异常时,程序段中throw就会抛出这个异常。

       最后来看try的catch子句,catch子句后括号里的异常类型的声明,在语法上与函数的形参类似,可以是某个类型(包括类)的值也可以是引用,它指明了此catch子句用来处理何种类型的异常。当try子句中的异常抛出后,每个catch子句会被依次检查,哪个catch子句的异常类型的声明与抛出异常的类型一致,就由哪个catch子句来处理此异常。catch后异常类型的声明部分可以是一个省略号,形式如:catch(...),这种形式的catch子句可以处理任何类型的异常,它只能放到try块所有其他catch语句之后。

       可见,如果try监测的某段程序多个地方需要抛出异常,那么throw后应该跟不同类型的表达式来区分,而不应该只通过不同的值区分。

鸡啄米:C++编程入门系列之五十(异常处理)

       讲了异常处理的语法形式,据此再来说说异常处理的执行过程:

       1.程序正常执行到try块的try子句,然后执行try子句后的复合语句,也就是被监测的程序段。

       2.如果try子句后的程序段正常执行了,没有发生任何异常,那么此try块的所有catch子句将不被执行,程序直接跳转到整个try块(包括try子句和所有catch子句)后继续执行。

       3.如果try子句后的程序段或者此程序段中的任何调用函数发生了异常,并通过throw抛出了这个异常,则此try块的所有catch子句会按其出现的顺序被检查。若没有找到匹配的处理程序则继续检查外层的try块。如果一直找不到则此过程会继续到最外层的try块被检查。这里有两种结果:a.找到了匹配的处理程序,则相应catch子句捕捉异常,将异常对象拷贝给catch的参数,如果此参数是引用则它指向异常对象。catch的参数被这样初始化以后,此catch子句对应的try子句后的程序段中,从开头到异常抛出位置之间构造的所有对象进行析构,析构顺序与构造顺序相反。然后catch处理程序被执行,最后程序跳转到try块之后的语句执行。b.始终没有找到匹配的处理程序,则运行C++库函数terminate,而terminate函数调用abort函数终止程序。

       鸡啄米给大家举个异常处理的例子,大家知道我们只能对非负实数求平方根,若是负数就应该处理此异常。例程如下:

       #include <iostream>
       #include "math.h"
       using namespace std;
       double GetSqrt(double x);       // 求平方根的函数的原型声明
       int main()
       {
                try
                {
                           // 由于求平方根运算有可能出现对负数运算的异常,所以放到try块中
                          cout << "9.0的平方根是 " << GetSqrt(9.0) << endl;
                          cout << "-1.0的平方根是 " << GetSqrt(-1.0) << endl;
                          cout << "16.0的平方根是 " << GetSqrt(16.0) << endl;
                }
               catch(double y)    // 捕捉double型异常
               {
                          cout << "发生对负数" << y << "求平方根的异常。" << endl;
               }
               cout << "程序继续运行完毕。" << endl;
               return 0;
       }
       double GetSqrt(double x)
       {
               if (x < 0)
                         throw x;     // 如果x为负数,则抛出一个double型异常
               return sqrt(x);
       }

       程序运行结果为:

       9.0的平方根是 3
       发生对负数-1求平方根的异常。
       程序继续运行完毕。

       根据结果可以看出,程序在运行cout << "-1.0的平方根是 " << GetSqrt(-1.0) << endl;时GetSqrt函数抛出异常,异常被main函数中catch子句捕捉,输出信息后,程序跳转到main函数最后一句输出"程序继续运行完毕。"。而try子句后的cout << "16.0的平方根是 " << GetSqrt(16.0) << endl;没有被执行。这是因为异常抛出后会按照catch子句出现的顺序依次检查,当找到匹配的catch处理程序时后面的所有catch子句就被忽略。根据这个原理,若catch(...)放到前面则其后的所有catch子句就不会被检查,因此它只能放到try块的最后。

       其实很多情况下catch子句的处理程序并不需要访问异常对象,只需要声明异常的类型就够了,例如上面程序中的catch子句就可以改成

        catch(double)    // 捕捉double型异常
        {
             cout << "发生对负数求平方根的异常。" << endl;
        }

       当然如果需要访问异常对象就要给出参数名,就像上面程序中的catch(double y)。

       三.异常接口声明

       我们可以在函数的声明中给出它可能会抛出的所有异常类型。例如:

       void func()  throw(X, Y);

       上面的语句表明函数func能够抛出也只能抛出X、Y及它们子类型的异常。

       若函数的声明中没有给出任何异常接口声明,则此函数可能抛出任何类型的异常。例如:

       void func();

       如果函数不抛出任何类型的异常,则可以这样声明:

       void func()  throw();

       这一讲的内容比较多,希望大家好好掌握,多多练习。这是本教程最后一讲,鸡啄米希望大家学完以后能够实践和应用起来,最终成功加入到C++开发队伍中来。大家有问题可以经常回鸡啄米博客来交流讨论。