C++ 异常处理
异常处理这部分内容其实并不属于OPP的技术,仅仅是C++对程序出错的处理。
异常处理
程序中常见的错误有两大类:语法错误和运行错误。在编译时,编译系统能发现程序中的语法错误。
在设计程序时,应当事先分析程序运行时可能出现的各种意外的情况,并且分别制订出相应的处理方法,这就是程序的异常处理的任务。
在运行没有异常处理的程序时,如果运行情况出现异常,由于程序本身不能处理,程序只能终止运行。如果在程序中设置了异常处理机制,则在运行情况出现异常时,由于程序本身已规定了处理的方法,于是程序的流程就转到异常处理代码段处理。
异常(exception)是运行时(run-time)的错误,通常是非正常条件下引起的,例如,下标(index)越界、new操作不能正常分配所需内存。
C语言中,异常通常是通过被调用函数返回一个数值作为标记的。
C++采取的办法是:如果在执行一个函数过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用它的函数),它的上级捕捉到这个信息后进行处理。如果上一级的函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上送,如果到最高一级还无法处理,最后只好异常终止程序的执行。
C++中,函数可以识别标记为异常的条件,然后通告发生了异常。
这种通告异常的机制称为抛出异常(throwing an exception)。
为什么要使用异常处理?
1.常规代码与错误处理代码的分离
用伪代码描述下面这个程序的各部分功能
readFile
{
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
What happens if the file can't be opened?
What happens if the length of the file can't be determined?
What happens if enough memory can‘t be allocated(分配)?
What happens if the read fails(失败)?
What happens if the file can't be closed?
面对这些可能发生的问题我们需要在编写程序的时候就要考虑到,如果采用非异常处理的方法会得到下面的代码:
errorCodeType readFile { initialize errorCode = 0; open the file; if (theFileIsOpen) { determine the length of the file; if (gotTheFileLength) { allocate that much memory; if (gotEnoughMemory) { read the file into memory; if (readFailed) { errorCode = -1; } } else { errorCode = -2; } } else { errorCode = -3; } close the file; if (theFileDidntClose && errorCode == 0) { errorCode = -4; } else { errorCode = errorCode and -4; } } else { errorCode = -5; } return errorCode; }
我们为了确定到底在代码执行的那一个部分出错,嵌套了太多的if语句,这显然是我们不能接受的。如果采用C++的异常处理得到下面的代码:
readFile { try { open the file; determine its size; allocate that much memory; read the file into memory; close the file; } catch (fileOpenFailed) { doSomething; } catch (sizeDeterminationFailed) { doSomething; } catch (memoryAllocationFailed) { doSomething; } catch (readFailed) { doSomething; } catch (fileCloseFailed) { doSomething; } }
实现了常规代码与错误处理代码的分离。
2.在调用栈中传播异常
method1
{
call method2;
}
method2
{
call method3;
}
method3
{
call readFile;
}
如果有上面这样的函数嵌套,我们想要确定到底在那一个函数中出错,如果采用非异常处理的方法会得到下面的代码:
method1 { errorCodeType error; error = call method2; if (error) doErrorProcessing; else proceed; } errorCodeType method2 { errorCodeType error; error = call method3; if (error) return error; else proceed; } errorCodeType method3 { errorCodeType error; error = call readFile; if (error) return error; else proceed; }
异常处理方式:
method1 { try { call method2; } catch (exception e) { doErrorProcessing; } } method2 { try { call method3; } catch (exception e) { doErrorProcessing; } } method3 { try { readFile; } catch (exception e) { doErrorProcessing; } }
3.对不同的错误类型进行分类
catch (FileNotFoundException e) { ... } catch (IOException e) { … …. } … catch (Exception e) //A (too) general exception handler { ... }
异常处理方式
C++中,try与catch用于实现异常的处理。
C++处理异常的机制是由3个部分组成的,即检查(try)、抛出(throw)和捕捉(catch)。
把需要检查的语句放在try块中,throw用来当出现异常时发出一个异常信息,而catch则用来捕捉异常信息,如果捕捉到了异常信息,就处理它。
Catch块可以任何顺序排列。
一个关键要求是catch必须定义在try块之后,在try块中可能会有异常被抛出(发生)。
异常与catch是以类型来进行匹配的。
其中异常也分两种,一种是系统定义的异常,另一种是用户自定义的异常。
1.系统定义异常
例如下面的程序,删除字符串中的一部分内容,一旦要删除的内容造成越界,系统就会抛出异常。
#include <iostream> #include <string> #include <stdexcept> using namespace std; int main() { string s = "Mike Jackson"; int index, len; while(true) { cout << "Enter index and length to erase: "; cin >> index >> len; try { s.erase(index, len); } catch ( out_of_range ) // out_of_range is a system-defined type { continue; } cout<<s; break; } return 0; }
3.用户自定义异常
下面的代码为了得到正确的数组第i个数的值,需要自己按照数组的规模进行限制。
const int MaxSize = 1000; float arr[MaxSize]; enum out_of_bounds { underflow, overflow }; float& access( int i ) { if ( i < 0 ) throw underflow; if ( i > MaxSize ) throw overflow; return arr[i]; } void g( ) { // … try { val = access( k ); } catch( out_of_bounds t) { if ( t == underflow ) { cerr << "arr: underflow…aborting\n"; exit( EXIT_FAILURE ); } if( t == overflow ) { cerr << "arr: overflow…aborting\n"; exit( EXIT_FAILURE ); } //… } }
通常,如果一个函数抛出了一个异常,但没有对应的catch处理它,则系统通过调用函数unexpected 函数去处理它。
实际上(In effect), unexpected 是没有被程序员处理的异常的缺省处理者。
函数嵌套检测异常处理
int main( ) { try { f1( ); } catch(double) { cout<<″OK0!″<<endl; } cout<<″end0″<<endl; return 0; } void f1( ) { try { f2( ); } catch(char) { cout<<″OK1!″; } cout<<″end1″<<endl; } void f2( ) { try { f3( ); } catch(int) { cout<<″Ok2!″<<endl; } cout<<″end2″<<endl; } void f3( ) { double a=0; try { throw a; } catch(float) { cout<<″OK3!″<<endl; } cout<<″end3″<<endl; }
其异常处理如图所示:
程序运行结果如下:
OK0! (在主函数中捕获异常)
end0 (执行主函数中最后一个语句时的输出)
该程序在f3函数的时候抛出了一个异常,但f3函数没有去捕获,f2、f1也都没有捕获,在主函数的时候被捕获到。
(2) 如果将f3函数中的catch子句改为catch(double),而程序中其他部分不变,则程序运行结果如下:
OK3! (在f3函数中捕获异常)
end3 (执行f3函数中最后一个语句时的输出)
end2 (执行f2函数中最后一个语句时的输出)
end1 (执行f1函数中最后一个语句时的输出)
end0 (执行主函数中最后一个语句时的输出)
该程序在f3函数的时候抛出了一个异常,立刻被f3函数捕获,将执行完f3函数,f3函数的执行完成意味着主函数、f1、f2的调用完成。
(3)如果在此基础上再将f3函数中的catch块改为
catch(double) { cout<<″OK3!″<<endl; throw; }
程序运行结果如下:
OK3! (在f3函数中捕获异常)
OK0! (在主函数中捕获异常)
end0 (执行主函数中最后一个语句时的输出)
该程序在f3函数的时候抛出了一个异常,立刻被f3函数捕获,然后又抛出一个新的异常,这个异常直到主函数才被捕获。
异常处理实例
编写一个计算三角形面积的函数,函数的参数为三角形三边边长a、b、c,可以用Heron公式计算:
#include <iostream> #include <cmath> #include <stdexcept> using namespace std; //给出三角形三边长,计算三角形面积 double area(double a, double b, double c) throw (invalid_argument) { //判断三角形边长是否为正 if (a <= 0 || b <= 0 || c <= 0) throw invalid_argument("the side length should be positive"); //判断三边长是否满足三角不等式 if (a + b <= c || b + c <= a || c + a <= b) throw invalid_argument("the side length should fit the triangle inequation"); //由Heron公式计算三角形面积 double s = (a + b + c) / 2; return sqrt(s * (s - a) * (s - b) * (s - c)); } int main() { double a, b, c; //三角形三边长 cout << "Please input the side lengths of a triangle: "; cin >> a >> b >> c; try { double s = area(a, b, c); //尝试计算三角形面积 cout << "Area: " << s << endl; } catch (exception &e) { cout << "Error: " << e.what() << endl; } return 0; }
运行结果1:
Please input the side lengths of a triangle: 3 4 5
Area: 6
运行结果2:
Please input the side lengths of a triangle: 0 5 5
Error: the side length should be positive
运行结果3:
Please input the side lengths of a triangle: 1 2 4
Error: the side length should fit the triangle inequation
下面介绍异常处理需要注意的几点:
(1) 首先把可能出现异常的、需要检查的语句或程序段放在try后面的花括号中。被检测的函数必须放在try块中,否则不起作用。
(2) try块和catch块作为一个整体出现,catch块是try-catch结构中的一部分,必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句。但是在一个try-catch结构中,可以只有try块而无catch块。
程序开始运行后,按正常的顺序执行到try块,开始执行try块中花括号内的语句。如果在执行try块内的语句过程中没有发生异常,则catch子句不起作用,流程转到catch子句后面的语句继续执行。一个try-catch结构中只能有一个try块,但却可以有多个catch块,以便与不同的异常信息匹配。
(3)try和catch块中必须有用花括号括起来的复合语句,即使花括号内只有一个语句,也不能省略花括号。
(4) 如果在执行try块内的语句(包括其所调用的函数)过程中发生异常,则throw运算符抛出一个异常信息。throw抛出异常信息后,流程立即离开本函数,转到其上一级的函数(main 函数)。
throw抛出什么样的数据由程序设计者自定,可以是任何类型的数据。
(5) 这个异常信息提供给try-catch结构,系统会寻找与之匹配的catch子句。
(6) 在进行异常处理后,程序并不会自动终止,继续执行catch子句后面的语句。
(7) catch只检查所捕获异常信息的类型,而不检查它们的值。因此如果需要检测多个不同的异常信息,应当由throw抛出不同类型的异常信息。异常信息可以是C++系统预定义的标准类型,也可以是用户自定义的类型(如结构体或类)。
(8) 如果在catch子句中没有指定异常信息的类型,而用了删节号“…”,则表示它可以捕捉任何类型的异常信息,如:
catch(…)
{
cout<<″OK″<<endl;
}
try-catch结构可以与throw出现在同一个函数中,也可以不在同一函数中。当throw抛出异常信息后,首先在本函数中寻找与之匹配的catch,如果在本函数中无try-catch结构或找不到与之匹配的catch,就转到离开出现异常最近的try-catch结构去处理。
(9) 如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行。