引入异常
假设有A,B,C三个函数,其中A调用了B,B调用了C(A > B > C)。
如果C执行过程中出现异常,那么首先要将异常情况返回给B,然后由B再返回给A。
在C++中,可以使用异常来处理这种情况。
可以简单概括为:函数A捕捉函数C发出的异常。
分析一下这句话:
- 谁捕捉异常?—— A
- 谁抛出异常?—— C
- 捕捉到异常后怎么处理?—— 由A决定
修改代码,在函数C中抛出一个异常,异常值为1;对应的,在函数A中会捕捉这个异常,并且将异常值输出。
void C()
{
throw 1;
}
void B()
{
C();
}
void A()
{
try {
B();
} catch (int i)
{
cout << "catch exception " << i << endl;
}
}
main函数
int main(int argc, char **argv)
{
A();
return 0;
}
编译测试,可以看到函数A成功捕捉到了函数C抛出的异常。
修改测试
修改C函数,增加一个传参,在代码中根据传参的值选择程序正常执行还是抛出异常。
void C(int i)
{
int a = 1;
double b = 1.23;
float c = 4.56;
if (i == 0) {
cout << "In C, it is OK" << endl;
} else if (i == 1) {
throw a;
} else if (i == 2) {
throw b;
} else if (i == 3) {
throw c;
}
}
C的传参是由A传给B,B传给C的,也就是说来源是A。
void B(int i)
{
cout << "call C ..." << endl;
C(i);
cout << "After call C" << endl;
}
void A(int i)
{
try {
B(i);
} catch (int j) {
cout << "catch exception " << j << endl;
}
}
修改main函数,将传入的argv[1]从中字符转为整型,然后传给函数A,也就是说C的传参来源是命令行。
int main(int argc, char **argv)
{
int i;
if (argc != 2) {
cout << "Usage: " << endl;
cout << argv[0] << " <0|1|2|3>" << endl;
return -1;
}
i = strtoul(argv[1], NULL, 0);
A(i);
return 0;
}
编译测试,当执行 ./exception2 0 时,程序正常执行,没有发生异常。
当执行 ./exception2 1 时,捕捉到了异常,并且输出了异常值1。
当执行 ./exception2 2 时,程序崩溃了。
这是因为,传入2时C抛出了一个double型的异常值,但是A捕捉的异常是int型,此时程序崩溃了。
完善测试程序
我们需要完善一下测试程序,当C函数抛出不同类型的异常值时,程序不应该崩溃。
修改A函数,增加对double型和float型异常值的捕捉和处理。
此时再执行 ./exception3 2 和 ./exception3 3 就没有问题了。
但是,如果异常的类型非常的多,难道每个类型都要加一个专门的处理吗?是否可以写一段代码,直接处理剩下的所有种类的异常?
答:可以的。
修改A函数,将对float型异常值的处理,改为对剩下的所有种类的异常的处理。
编译测试,此时对float型异常值的处理流程,就是省略号中定义的处理了。
对 double 和 int 则没有影响。
抛出类的实例化对象
在之前的程序中,throw出的是int,double,float,那么是否可以扔出类的实例化对象呢?
答:可以的。
增加一个类,叫做MyException:
class MyException {
public:
void what(void)
{
cout << "MyException::what(void)" << endl;
}
};
然后在C函数中,传参为4时抛出这个类异常。
在A函数中,捕捉到抛出了MyException类异常时,调用成员函数what。
编译测试,程序成功抛出和捕捉到了异常。
增加派生类异常
修改代码,增加一个 MyException类的派生类 MySubException。
class MySubException : public MyException {
public:
void what(void)
{
cout << "MySubException::what(void)" << endl;
}
};
修改代码,当i=5时,抛出的类异常是MySubException。
此时如果不修改A函数,即不添加MySubException类异常的检测时,此时会触发哪个处理程序。
可以看到,此时会触发处理基类异常的异常处理程序,也就是说基类的检测,同样可以检测出抛出的派生类异常。
在A函数中,添加对MySubException类异常的处理。
添加在对MyException类异常的处理之后。
此时编译会有警告,意思是抛出的派生类MySubException类异常,会被之前处理基类MyException类异常的程序处理,而不是处理MySubException异常的代码来处理,因为catch MySubException在catch MyException之后。
执行发现,触发的处理程序也是MyException类的处理程序。
将对MySubException类异常的处理,转移到对MyException类异常的处理之前。
此时编译执行,可以成功触发对MySubException类异常的处理。
使用多态
修改代码,去掉对派生类MySubException异常的处理,只保留对基类MyException的处理。
此时,如果想要实现,抛出基类MyException类异常,调用的是基类的what函数;抛出派生类MySubException类异常,调用派生类的what函数。
那么,就要使用多态。
将MyException类的成员函数what,声明为 virtual 即可。
编译测试,结果符合预期。
增加可能抛出异常的说明
前面的代码,在C函数中抛出异常,在函数A中捕获异常并处理,那么,A怎么知道有哪些异常需要处理呢?或者说,它怎么知道函数C可能抛出哪些异常呢?
修改代码,在函数C中增加说明可能抛出的异常的说明,声明函数C可能抛出int型和double型的异常。
编译测试,此时只有抛出int型和double型的异常时,程序可以成功执行,抛出其他异常,程序都会崩溃。
也就是说,即时在A函数中有针对float型,MyException类,MySubException类异常的处理,但是由于C函数没有声明可能抛出这些异常,此时如果C函数抛出这些异常,程序也会崩溃。
未定义异常的处理和终止函数
未定义异常的处理函数
当程序检测到未定义异常时,如果我们没有定义一个未定义异常的处理函数,那么程序会调用一个默认的未定义异常的处理函数。
可以通过set_unexpected重新定义一个未定义异常的处理函数。
修改代码,增加一个未定义异常的处理函数my_unexpected_func。
编译测试,可以看到当检测到未定义异常时,调用了我们自定义的未定义异常的处理函数my_unexpected_func。
未定义异常终止函数
在上面的未定义异常的处理函数函数中,除了输出我们添加的my_unexpected_func(void)语句外,还有一条“terminate called after throwing an instance of 'MyException'”。
这个是由默认的未定义异常终止函数输出的,类似的,我们同样可以自定义一个未定义异常终止函数。
修改代码,使用set_terminate自定义一个未定义异常终止函数my_terminate_func。
编译测试,可以看到,在调用了我们自定义的未定义异常的处理函数之后,系统又调用了自定义的未定义异常的终止函数。
小结
对于意料之外的异常,系统会执行两个函数:
- “unexpected”函数(可以自己提供);
- “terminate”函数(可以自己提供);
其中,“unexpected”函数用来处理声明之外的异常;“terminate”函数用来处理“catch分支未提到的异常”。