代码改变世界

白话C++系列(28) -- 异常处理

2016-06-28 20:20  Keiven_LY  阅读(525)  评论(0编辑  收藏  举报

异常处理

所谓异常:程序运行期出现的错误

异常处理:对有可能发生异常的地方做出预见性的安排

如果我们做出的安排合理,并且能够给出人性化的提示,那么使用者就不会觉得突兀,使用者就会根据我们给出的提示做相应的操作。比如说,告诉使用者网线没有插,待插上网线才可以使用,否则没法联网;再比如,告诉使用者内存太小(内存不足),必须释放掉一些内存,程序才可以正常往下运行。这样,使用者就可以非常容易的去接收所给出的提示,并且可以根据相应的提示能够使得程序顺利的继续进行下去。如果有些异常,我们没有预料到,那么就会直接报给系统,系统则是非常粗暴的,它会将我们的程序直接杀死,对于用户来说,所看到的现象就是我们的程序直接崩溃掉,而程序崩溃是我们最不愿意看到的。程序崩溃对于开发者来说简直就是一场噩梦。那么,如何来进行一场处理呢?

关键字:

Try(尝试)…catch(捕获)…:即尝试运行正常的逻辑,如果在运行正常逻辑的时候出现异常,那么就会通过catch将其捕获,捕获之后再去对出现的异常进行处理

Throw(抛出异常):抛出异常之后,被catch进行捕获,捕获之后再进行处理。

基本思想:主逻辑与异常处理分离

接下来我们看一看异常处理在C++中是如何工作的??

如果我们定义三个函数:f1、f2、f3,我们用f2来调用f1,用f3来调用f2。如果f1在运行过程中出现了异常,那么它就会把出现的异常向上抛,抛给它的调用者f2,如果f2可以处理,那么就可以处理完成,如果f2处理不了,那就继续向上抛,抛给它的调用者f3,那么作为更上层的f3就会捕获到异常,捕获到之后就会进行相应的处理,如果处理不了就继续向上抛,直到有函数进行处理,如果所有的调用函数都不能处理,那么最后就会抛给操作系统,操作系统就会粗暴的进行干预。下面我们就通过例子进行进一步的说明。

我们定义了一个函数fun1,在fun1当中我们写出了”throw 1”(只是简单的抛出了一个数字1,实际的程序肯定不能这样写,一定会写出正常的逻辑),在main函数中,我们通过try…catch…来进行异常的捕获,我们将fun1这个函数的调用方在try块中,如果它能够正常的运行完成,那么catch块就不会执行,如果fun1不能够正常的完成调用,而在运行的过程中不幸出现问题而抛出数字1,那么此时就必须用catch块来捕获它,捕获之后,就可以在catch语句块中作相应的处理。请大家注意,我们在fun1中所抛出的是一个数字1,其是一个int类型,所以我们可以通过catch(int)才能够捕获到,如果我们所抛出的不是1,而是0.1,那么catch中就应该用double,即catch(double)来进行捕获。如下所示:

下面我们来看另外一种情况,对于try…catch…来说,它可以不是一对一的,它可以是一对多的,如下:

对于一个try来说,其里面有相应的主逻辑,主逻辑在运行的过程中,可能在第一行代码就抛出异常,也可能在第三行代码抛出异常,也可能在第四行代码抛出异常,所抛出的异常可能是int类型,也有可能是double类型,也有可能是其他类型的,那么这个时候我们就要根据不同的异常来做相应的处理。如果以上catch块都不能捕获到相应的异常,那么最后的一个catch块可以为大家兜底,请注意写法catach( … ),这种写法就是说我可以捕获所有的异常,所有的异常统统可以处理。可是这种处理是很野蛮的,因为我们不分青红皂白,没有细致划分,就一刀切的在catch块中写相应的代码,无非就是告诉用户:“你出错了,只能关闭”。所以我们并不建议大家这样写:直接try后跟一个catch( … ),而应该在前面所有的情况下都处理不了了,万般无奈下才使用。

在刚才的例子中,我们发现一个特点,我们所抛出的异常虽然是一个值,但是我们在捕获的时候只是一种数据类型。如果我们想要捕获这个值怎么办呢?我们来看一看下面这个例子。

在这个例子中,我们的函数名叫做getChar,是要获取一个字符,可是传入的两个参数呢,一个是字符串,一个则是下标。我们想要根据字符串并且通过下标拿到字符串中所对应下标的字符。可是你无法保证传入进来的下标就一定比字符串短,什么意思呢?比如你传入的字符串一共就3个字符,而你传入的下标是4,显然这是不符合逻辑的,那么既然不符合逻辑,我们就应该通过throw将这种异常跑出去,告诉外面的用户:“你当前传入的下标是非法的”。我们更希望把这段字符拿到,所以我们采用如下方式就可以拿到了。

大家请看,此时在catch中,写的是(catch(string& aval)),这个时候如果我们传入的字符串是“hello world”,而传入的下标是100,它必然会抛出string,然后告诉我们,传入的是一个非法下标(string(“invalid index”))。那么这个时候,我们就可以通过catch块拿到相应的值,并且把它打印出来,这样就清晰的告诉用户:“你的下标传错了”。

常见异常:

  •  数组下标越界
  •  除数为0
  •  内存不足

异常处理与多态的关系

比如我们定义一个异常类,叫做Exception,这个异常类假设是一个借口类,在它其中,我们定义一些打印的方法(异常处理的方法),然后我们通过细分的子类来继承借口类(Exception),那么,当我们抛出这些子类的对象的时候,就都可以用这个父类(Exception)去捕获,如下:

我们来看一个例子:

如果我们定义一个fun1的函数,在fun1中我们进行了相应的逻辑处理,在处理的过程中,不幸的跑出了一个SizeErr的异常(其是Exception的一个子类),在fun2函数中,也写了一些逻辑,如果这些逻辑出现异常,比如抛出了一个MemmoryErr的异常(其也是Exception的一个子类),这个时候,我们怎么去捕获它们呢??无论是fun1还是fun2,我们都可以通过try…catch…来捕获它们。如下:

这里的关键是,catch中我们用到了父类Exception,这个时候我们就可以捕获到fun1和fun2所抛出的子类对象,并且通过子类对象去调用相应的虚函数。

异常处理代码实践

题目描述:

/*  ************************************  */

/* 异常处理

         1. 定义一个Exception类,成员函数:printException,析构函数

         2. 定义一个IndexException类,成员函数printException 

         Note: Exception类是异常类,作为父类

               IndexException类是下标索引异常类,是Exception类的子类

            如果这两个类具有继承关系,我们需将父类的析构函数定义为虚析构函数

            父类和子类中的printException()都是虚函数

*/

/*  ************************************  */

程序框架:

头文件(Exception.h

#ifndef EXCEPTION_H
#define EXCEPTION_H

class Exception
{
public:
    virtual void printException();
    virtual ~Exception(){} //虚析构函数
};

#endif

源程序(Exception.cpp

#include"Exception.h"
#include<iostream>
using namespace std;

void Exception::printException()
{
    cout <<"Exception --> printException()"<< endl;
}

头文件(IndexException.h

#ifndef INDEXEXCEPTION_H
#define INDEXEXCEPTION_H

#include "Exception.h"
class IndexException : public Exception
{
public:
    virtual void printException();
};

#endif

源程序(IndexException.cpp)

#include "IndexException.h"
#include<iostream>
using namespace std;

void IndexException::printException()
{
    cout <<"提示:下标越界"<< endl;
}

主调程序(demo.cpp

首先演示一下对于普通的数据类型,如何使用try…catch…和throw的方式来进行异常处理。我们先来定义一个函数:test(),在这个test函数当中,我们不做其他逻辑,只通过throw将异常抛出,抛出的时候我们可以抛出一个数字10;然后,我们可以在main函数当中去调用test这个函数,调用的时候,我们用try…catch…来进行调用。如下所示:

#include<iostream>
#include"stdlib.h"
#include "IndexException.h"

using namespace std;

void test()
{
    throw 10;//抛出异常 10

}
int main()
{
    try
    {
        test();
    }
    catch(int)
    {
        cout <<"exception"<< endl;
    }
    system("pause");
    return 0;
}

我们来看一看有什么效果,按一下F5,看运行结果如下:

我们看到,打印出了“exception”的字样,可见,对于抛出来的10,我们是可以捕获到的。为了能够让大家看到有捕获不到的情况,我们在这可以不抛出10,可以抛出其他的东西,比如说,我们抛出一个0.1,我们来看一看现在可不可以捕获到,按F5运行如下:

可见,如果我们抛出的是0.1的话,计算机就无法捕获到,那么程序就会产生崩溃的状况。如果我们仍然抛出的是0.1,而把catch后面括号中的int改成double,这样再按F5,看看运行结果如下:

我们看到,此时程序没有崩溃,并且还打印出了“exception”的字样。从这就能够充分说明一个问题:如果你在抛出异常的时候,你抛出了如果你能看得到,那么你就能够进行合理的处理,或者说,你有能力对其进行处理;如果抛出的异常你捕获不到,等待计算机替你处理,那么系统就会崩溃。

接下来,我们通过引用来获取抛出来的异常值,给这个异常值所取的引用名为e,如下:

#include<iostream>
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw 0.1; //抛出异常 10

}
int main()
{
    try
    {
        test();
    }
    catch(double&e)
    {
        cout << e << endl;
    }
    system("pause");
    return 0;
}

我们来看一看,能不能把抛出的异常值0.1打印出来,我们按F5看一看运行结果:

我们看到,屏幕上打印出了0.1。可见,通过引用的方法式可以获取到抛出来的异常值的。如果我们在这里抛出来的不是一个数值,而是一个字符串,那就会直接打印出来一个字符串来了。在实际的项目当中,我们往往抛出来的是一个错误编号,catch到错误编号后,我们就可以根据错误编号找到相应的错误提示。

下面为大家展示一下多个Exception类之间所形成的继承关系在异常处理党章所展示出来的优越性。

首先,我们需要对程序改造一下,如下:

#include<iostream>
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw IndexException();

}
int main()
{
    try
    {
        test();
    }
    catch(IndexException &e)
    { 
        e.printException();
    }
    system("pause");
    return 0;
}

我们按F5看一下运行结果,如下:

通过运行结果,我们可以看到,我们捕获到了IndexException,并且打印出了下标越界的提示来。这样,用户就能知道自己犯了什么错误,可以通过程序的输入来弥补这样的错误。如果我们在这捕获的不是IndexException,而是Exception,能不能捕获到IndexException呢?我们来看一看,

#include<iostream>
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw IndexException();

}
int main()
{
    try
    {
        test();
    }
    catch(Exception&e)
    { 
        e.printException();
    }
    system("pause");
    return 0;
}

按一下F5,看一看运行结果,如下:

通过打印结果我们可以看到,通过Exception来捕获IndexException也是可行的。捕获到之后,打印出来的也是IndexException中定义的printException函数。如果我们在catch这部分中捕获的不是Exception,或者说,我们也不知道test函数会抛出什么样的异常,这就要catch后面括号中携程“…”的形式了,这样是否能够捕获到IndexException的异常呢?其实是可以捕获到的,但是我们没有办法调用异常中的成员函数了,因为这样的捕获实在是太笼统,我们没有办法获得更多的线索,通过这些线索来给用户更为精准的提示。我们来试一试,在这里我们只能打印“Exception”的字样,因为没有其他线索了,如下:

#include<iostream>
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw IndexException();

}
int main()
{
    try
    {
        test();
    }
    catch(...)
    { 
        cout <<"Exception"<< endl;
    }
    system("pause");
    return 0;
}

按F5来看一看运行结果:

大家可以看到,通过catch(…)也是可以捕获到异常的,只不过这时候捕获到的异常很无力,给出的提示很笼统。