二十一. 异常处理
● 异常的概念
程序的错误通常包括:语法错误、逻辑错误、运行异常。 语法错误指书写的程序语句不合乎编译器的语法规则,这种错误在编译、连接时由编译器指出。 逻辑错误是指程序能顺利运行,但是没有实现预期的功能,这类错误通过调试与测试发现。 操作等。 异常处理: 程序运行异常虽然是无法避免,但是可以预料,为了保证程序的健壮性,必须要在程序中对运行异常进行预见性处理,对运行异常进行预见性处理称为异常处理。 |
处理异常的基本思想是:在底层发生的问题,逐级上报,直到有能力可以处理异常的那级为止。或者说,在应用程序中,如果某个函数发现了错误并引发异常,这个函数就将该异常向上级调用者传递,请求调用者捕获该异常并处理该错误。如果调用者不能处理该错误,就继续向上级调用者传递,直到异常被捕获错误被处理为止。 如果程序最终没有相应的代码处理该异常,那么该异常最后被C++系统所接受,C++系统就简单地终止程序运行。 |
处理异常的方法很多,其中最直接的办法是调用C++中的exit()或abort()函数终止程序的执行,exit() 与abort()函数原型在头文件stdlib.h中声明. 两者的区别是exit()在中止程序运行前,会关闭被程序打开的文件、调用全局和static类型对象的析构函数等;而abort()直接结束进程, 什么都不做。 使用exit()与abort()来处理异常显得很机械,有的异常需要进行更复杂的处理。
|
※ 用if语句处理异常的问题: 来捕获、防止异常: float quotient(int a, int b) { return a/(float)b; } … cin>>a>>b; if (b==0) //捕获异常 cout<<"Divide 0 !"<<endl; else cout<<a<<"/"<<b<<"="<<quotient(a,b); 这种处理机制有如下缺点: (1) 每使用quotient()一次, 就必须利用if语句检查一次,使得程序对正常执行过程的描述与对异常的处理交织在一起,程序的易读性不好。 (2) 若异常信息在函数中返回,会破坏程序的逻辑性。如:原来没有返回值的函数,要定义成返回值;对原来有返回值的函数无法定义异常信息返回;象构造函数、析构函数这类由程序自动调用,又没有返回值的特殊函数,就没有办法利用返回值返回异常。 为此,C++提供了异常处理解决方案。 |
● 异常处理的语法
● 抛掷异常的程序段 ...... throw异常类型表达式; //也可以写成: throw(异常类型表达式); 这一表达式就是引发异常的东西 ...... ● 捕获并处理异常的程序段 try 可能产生错误的语句 ) {异常处理语句块1} ) {异常处理语句块1} … catch(异常类型n) {异常处理语句块n} 部分: ① throw 表达式 ② try 语句块 ③ catch 语句块
//异常处理的执行过程如下: (1) 执行try块中的程序语句序列; (2) 如果执行期间没有执行到throw()(没有引起异常),跳过异常处理区的catch语句块,程序向下执行; (3) 若执行期间引起异常,执行throw()语句抛出异常,进入异常处理区,将throw() 抛出的异常类型表达式(对象)依次与catch()中的类型匹配,获得匹配的catch子句将捕获并处理异常。继续执行异常处理区后的语句; (4) 如果未找到匹配(异常未捕获到),自动调用结束函数terminate(),其缺省功能是调用abort()终止程序。 |
● 处理除零异常的几种形式
#include <iostream> using namespace std; int divide(int x, int y) { if (y == 0) throw y; //用throw抛出异常, 即除数为0的异常(error of the divisor being zero) return x / y; } int main() { try //用try定义异常 { cout << "5 / 2 = " << divide(5, 2) << endl; cout << "8 / 0 = " << divide(8, 0) << endl; //发生异常, 函数divide()被退栈处理, 返回地址(即下一语句的地址)也被退栈, cout << "7 / 1 = " << divide(7, 1) << endl; //故这一语句不再被执行 } catch (int e) //用catch捕获并处理异常; 异常类型为int型; 定义一个int型变量e(即0), 并将捕获的异常的值赋给e { cout << e << " can't be a divisor!" << endl; } cout << "That is ok." << endl; return 0; } |
#include<iostream.h> //包含头文件 #include<stdlib.h> double fuc(double x, double y) //定义函数 { if(y==0) { throw y; //也可以是throw x, 因为C++使用数据类型来捕获不同的异常(这里的x, y都是double型, 所以x和y都可以作为被抛出的异常), 但因为这里引发异常的是y, 所以最好抛出y } return x/y; //否则返回两个数的商 } void main() { double res; try //定义异常 { res=fuc(2,3); cout<<"The result of x/y is : "<<res<<endl; res=fuc(4,0); //出现异常 } catch(double) //捕获并处理异常, 也可以写成catch(double y), 即catch的是类型, 而不是某个具体变量 { cerr<<"error of diviso being zero.\n"; //cerr是接受标准错误输出的对象 exit(1); //异常退出程序 } } |
# include <iostream> using namespace std; float quotient(int a,int b) throw(char *) //throw(char *)表示只抛出char *类型的异常 { if (b==0) throw "Divided by 0!"; else return a/(float)b; } void main() { int a,b; cout<<"Input a, b: \n"; cin>>a>>b; try { cout<<a<<"/"<<b<<"="<<quotient(a,b); } catch(char *ErrS) //定义一个char*型变量ErrS, 并将捕获的异常的值(即字符串"Divide 0!")赋给ErrS { cerr<<ErrS<<endl; } } |
● 异常接口声明
• 可以在函数的声明中列出这个函数可能抛掷的所有异常类型。 例如: void fun() throw(A,B,C,D); //表示fun只能抛出A,B,C,D类型的异常 • 若无异常接口声明,或者throw(...),则此函数可以抛掷任何类型的异常。 ※ 直接写throw(...), 这里的省略号不代表其它内容. • 不抛掷任何类型异常的函数声明如下: void fun() throw(); |
● 异常处理中的构造和析构
#include<iostream.h> class expt //定义类expt { public: //定义公有成员 expt() //定义构造函数 { cout<<"structor of expt"<<endl; } ~ expt() //定义析构函数 { cout<<"destructor of expt"<<endl; } }; class demo //定义类demo { public: demo() //定义构造函数 { cout<<"structor of demo"<<endl; } ~demo() //定义析构函数 { cout<<"destructor of demo"<<endl; } }; void fuc1() //定义函数 { int s=0; demo d; //声明demo类的对象 throw s; //抛出异常, 这里的异常抛出没有条件, 反正就是有异常 } void fuc2() { expt e; //声明expt类的对象 fuc1(); //调用函数fuc1 } void main() { try //定义异常 { fuc2(); //调用函数 } catch(int) //定义异常处理 { cout<<"catch int exception"<<endl; } cout<<"continue main()"<<endl; } |
//在抛出异常前, e和d这两个对象先后被创建, 在抛出异常后, 两个对象按与创建的相反顺序调用析构函数而被销毁. |
● 异常处理综合案例
求一元二次方程(ax²+bx+c=0 (a≠0))的实根, 要求加上异常处理, 判断b*b-4*a*c是否大于0, 成立则求两个实根, 否则要求重新输入. 注意, 一元二次方程求根公式为: |
#include <iostream> #include <math.h> using namespace std; double sqrt_delta(double d) { if(d < 0) throw 1; return sqrt(d); } double delta(double a, double b, double c) { double d = b * b - 4 * a * c; return sqrt_delta(d); } void main() { double a, b, c; cout << "please input a, b, c" << endl; cin >> a >> b >> c; //接收输入 while(true) //无限循环, 这样如果delta小于0, 我们可以重新输入系数a, b, c { try { double d = delta(a, b, c); cout << "x1: " << (d - b) / (2 * a); cout << endl; cout << "x2: " << -(b + d) / (2 * a); cout << endl; break; //跳出循环 } catch(int) { cout << "delta < 0, please reenter a, b, c."; cin >> a >> b >> c; } } } |
● 标准异常类
C++提供了标准异常处理类库,它用来抛出C++标准库中函数执行时的异常。C++标准异常处理类的层次结构图如下图所示:
例如: logic_error表示可以在程序中被预先检测到的异常(如果小心地编写程序,这类异常能够避免); runtime_error表示难以被预先检测的异常 ※ 基类exception提供了一个成员函数what(), 用于返回错误信息(返回类型为const char*)该函数在exception的派生类中可以被重载. |
● 标准异常类的使用
# include <iostream> //# include <new> //在Visual C++6.0中可以不包含 # include <string> //# include <stdexcept> //在Visual C++6.0中可以不包含 using namespace std; void main() { string* S; //S是对象 try { S=new string("ABCD"); //可能抛出bad_alloc异常 cout<<S->substr(5,2); //可能抛出out_of_range异常 } catch(bad_alloc& NoMemory) //bad_alloc&是异常类型, 异常的值传给了对象NoMemory { cout<<"Exception occurred: "<<NoMemory.what()<<endl; //what()是对象NoMemory的成员函数 } catch(out_of_range& OutOfRange) // out_of_range&是异常类型, 异常的值传给了对象OutOfRange { cout<<"Exception occurred: "<<OutOfRange.what()<<endl; //what()是对象OutOfRange的成员函数 } } |