现在的位置: 首页 > 综合 > 正文

第二十四讲:异常处理

2013年08月30日 ⁄ 综合 ⁄ 共 8921字 ⁄ 字号 评论关闭

第二十四讲:异常处理

本讲基本要求

    * 掌握;函数声名中异常情况的指定;异常处理中处理析构函数; 异常继承以及C++标准库的异常类层次结构。
    * 理解:异常的任务及解决的方法。
    * 了解:异常和继承以及C++标准库的异常类层次结构。
重点、难点
    ◆函数声名中异常情况的指定;异常处理中处理析构函数; 异常继承以及C++标准库的异常类层次结构。

   在C++发展的后期,有的C++编译系统根据实际工作的需要,增加了一些功能,作为工具来使用,其中主要有模板(包括函数模板和类模板)、异常处理、命名空间和运行时类型,以帮助程序设计人员更方便地进行程序的设计和调试工作。1997年ANSI C++委员会它们纳入了ANSII C++标准,建议所有的C++编译系统都能实现这些功能。这些工具是非有用的,C++的使用者应当尽量使用这些工具,因此本书对此作简要的介绍,以便为日后的进一步学习和使用打下初步基础。

   在第3章的11节已介绍了类模板。在本章中主要介绍异常处理和命名空间,应当注意,期的C++是不具备这些功能的,只有近期的C++系统根据ANSIC++的要求,实现了这些功。请读者注意使用的C++版本。

一、异常处理的任务

   程序编制者总是希望自己所编写的程序都是正确无误的,而且运行结果也是完全正确的。但是这几乎是不可能的,智者千虑,必有一失,不怕一万,就怕万一。因此,程序编制者不仅要考虑程序没有错误的理想情况,更要考虑程序存在错误时的情况,应该能够尽快地发现错误,消除错误。

语法错误:
   
在编译时,编译系统能发现程序中的语法错误(如关键字拼写错误,变量名未定义,语句末尾缺分号,括号不配对等),编译系统会告知用户在第几行出错,是什么样的错误。由于是在编译阶段发现的错误,因此这类错误又称编译错误。有的初学者写的并不长的程序,在编译时会出现十几个甚至几十个语法错误,有人往往感到手足无措。但是,总的来说,这种错误是比较容易发现和纠正的,因为它们一般都是有规律的,在有了一定的编译经验以后,可以很快地发现出错的位置和原因并加以改正。

运行错误:
   
有的程序虽然能通过编译,也能投入运行。但是在运行过程中会出现异常,得不到正确的运行结果,甚至导致程序不正常终止,或出现死机现象。
   例如:  .在一系列计算过程中,出现除数为0的情况
         .内存空间不够,无法实现指定的操作。
         .无法打开输入文件,因而无法读取数据。
         .输入数据时数据类型有错。

   由于程序中没有对此的防范措施,因此系统只好终止程序的运行。这类错误比较隐蔽,易被发现,往往耗费许多时间和精力,这成为程序调试中的一个难点。

    在设计程序时,应当事先分析程序运行时可能出现的各种意外的情况,并且分别制订出相应的处理方法,这就是程序的异常处理的任务。

   需要说明,在一般情况下,异常指的是出错(差错),但是异常处理并不完全等同于对出错的处理。只要出现与人们期望的情况不同,都可以认为是异常,并对它进行异常处理。例如,在输入学生学号时输入了负数,此时程序并不出错,也不终止运行,但是人们认为这是不应有的学号,应予以处理。因此,所谓异常处理指的是对运行时出现的差错以及其他例外情况的处理。

二、异常处理的方法

C++处理异常的机制引入
   
在一个小的程序中,可以用比较简单的方法处理异常,例如用if语句判别除数是否为0,如果是。则输出一个出错信息。但是在一个大的系统中,包含许多模块,每个模块义包含许多函数,函数之间又五相调用,比较复杂。如果在每一个函数中都设置处理异常的程序段,会使程序过于复杂和庞大。因此,C++采取的办法是:如果在执行一个函数过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用它的函数),它的上级捕捉到这个信启后进行处理。如果上一级的函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上送,如果到最高一级还无法处理,最后只好异常终止程序的执行。
   这样做使异常的发现与处理不由同一函数来完成。好处是使底层的函数专门用于解决实际任务,而不必再承担处理异常的任务,以减轻底层函数的负担,而把处理异常的任务上移到某一层去处理。例如在主函数中调用十几个函数,只需在主函数中设置处理异常即可,而不必在每个函数中都设置处理异常,这样可以提高效率。

C++处理异常的机制组成:
   
C++处理异常的机制是由3个部分组成的,即检查(try)、抛出(throw)和捕捉(catch)。把需要检查的语句放在try块中,throw用来当出现异常时发出(形象地称为抛出,throw的意思是抛出)一个异常信息,而catch则用来捕捉异常信息,如果捕捉到厂异常信息,就处理它:

例1 给出三角形的三边a,b,c,求三角形的面积。只有a+b>c,b+c>a,c+a>b时才能构成三角形。设置异常处理,对不符合三角形条件的输出警告信息,不予计算。

#include <iostream>
#include <cmath>
using namespace std;
int main()
 { double triangle(double,double,double);
   double a,b,c;
   cin>>a>>b>>c;
   while(a>0 && b>0 && c>0)
      { cout<<triangle(a,b,c)<<endl;
        cin>>a>>b>>c; }
   return 0; }

double triangle(double a,double b,double c)
 { double area;
   double s=(a+b+c)/2;
   area=sqrt(s*(s-a)*(s-b)*(s-c));
   return area; }

运行情况如下
   6 5 4/ (输入a,b,c的值)
   
9.92157 (输出三角形的面积)
   
1 1.5 2/ (输入a,b,c的值)
   
0.726184 (输{¨三角形的面积}
   
1 2 1 / (输人a,b,c的值)
   
o (输出三角形的面积,此结果显然不对,因为不是三角形)
   
1 0 6/ (输入a,b,c的值) (结束)

   程序没有对三角形条件(任意两边之和应大于第三边)进行检查,因此,当输入a=l,b=2,c=1时,则计算出三角形的面积为0,显然是不合适的。
   现在修改程序,在函数triangle中对三角形条件进行检查,如果不符合三角形条件,就抛出一个异常信息,在主函数中的try-catch块中调用triangle函数,检测有无异常信息,并相应处理。修改后的程序如下:

#include <iostream>
#include <cmath>
using namespace std;
int main()
 { double triangle(double,double,double);
   double a,b,c;
   cin>>a>>b>>c;
   try //在try块中包含要检查的函数
    
{ while(a>0 && b>0 && c>0)
       { cout<<triangle(a,b,c)<<endl;
         cin>>a>>b>>c;}
  }
catch(double) //用catch捕捉异常信息并作相应处理
 
{ cout<<"a="<<a<<",b="<<b<<",c="<<c<<",that is not a traingle!"<<endl;}
   cout<<"end"<<endl;
   return 0; }

double triangle(double a,double b,double c) //计算三角形的面积的函数
 
{ double sqrt;
   double s=(a+b+c)/2;
   if (a+b<=c||b+c<=a||c+a<=b) throw a; //当不符合三角形条什抛出异常信息
   
return sqrt(s*(s-a)*(s-b)*(s-c)); }

程序运行结果如下:
   6 5 4 (输入a,b,c的值)

   9.92157
(计算出三角形的面积)
   1 1.5 2
(输入a,b,c的值)
   0.726184
(计算出三角形的面积)
   1 2 1
(输入a,b,c的值)
   a=1,b=2,c=1,that is not a triangle!
(异常处理)
   end

现在结合程序分析怎样进行异常处理。
   (1)首先把可能出现异常的、需要检查的语句或程序段放在try后面的花括号中。由于riangle函数是可能出现异常的部分,所以把while循环连同triangle函数放在try块中。这些语句是正常流程的一部分,虽然被放在by块中,并不影响它们按照原来的顺序执行。
   (2)程序开始运行后,按正常的顺序执行到try块,开始执行by块中花括号内的语句。如果在执行try块内的语句过程中没有发生异常,则catch子句不起作用,流程转到catch子句后面的语句继续执行。
   (3)如果在执行try块内的语句(包括其所调用的函数)过程中发生异常,则throw运算符抛出一个异常信息。请看程序中的triangle函数部分,当不满足三角形条件时,throw抛出double类型的异常信息a。throw抛出异常信息后,流程立即离开本函数,转到其上一级的函数(main函数)。因此不会执行triangle函数中if语句之后的return语句。
throw抛出什么样的数据由程序设计者自定,可以是任何类型的数据(包括自定义类型的据,如类对象)。
   (4)这个异常信息提供给try-catch结构,系统会寻找与之匹配的catch子句。现在a是double型,而catch子句的括号内指定的类型也是double型,二者匹配,即catch捕获了该异常信息,这时就执行catch子句中的语句,本程序输出a=1.b=2,c=1,that is not a triangle!
   (5)在进行异常处理后,程序并不会自动终止,继续执行catch子句后面的语句。本程序输出“end”。注意并不是从出现异常点继续执行while循环。如果在try块的花括号内有10个语句,在执行第3个语句时出现异常,则在处理完该异常后,其余7个语句不再执行,而转到catch子句后面的语句去继续执行。
由于catch子句是用来处理异常信息的,往往被称为catch异常处理块或catch异常处理器。

异常处理的语法:

throw语句一般是由throw运算符和一个数据组成的,其形式为:

   throw 表达式;

try-catch的结构为:

   try
      { 被检查的语句}
   catch(异常信息类型[变量名])
      { 进行异常处理的语句}

说明:
   
(1)被检测的函数必须放在try块中,否则不起作用。
   (2)try块和catch块作为—个整体出现,catch块是try-catch结构中的一部分,必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句,
   (3)try和catch块中必须有用花括号括起来的复合语句,即使花括号内只有一个语句,也不能省略花括号。
   (4)一个try-catch结构中只能有一个try块,但却可以有多个catch块,以便与不同的异常信息匹配。
   (5)catch后面的圆括号中,一般只写异常信息的类型名,如
catch(double)
   catch只检查所捕获异常信息的类型: 异常信息可以是C++系统预定义的标准类型,也可以是用户自定义的类型(如结构体或类)。如果由throw抛出的信息属于该类型或其子类型,则catch与throw二者匹配,catch捕获该异常信息。
   catch还可以有另外一种写法,即除了指定类型名外,还指定变量名,如:
      catch(double d)
   此时如果throw抛出的异常信息是double型的变量a,则catch在捕获异常信息a的同时,还使d获得a的值,或者说d得到a的一个拷贝。什么时候需要这样做呢?有时希望在捕获异常信息时,还能利用throw抛出的值,如
    (6)如果在catch子句中没有指定异常信息的类型,而用了删节号“…”,则表示它可以捕捉任何类型的异常信息,如: catch(…){cout<<"OK"<<endl;} 它能捕捉所有类型的异常信息,并输出”OK”。
    (7)try-catch结构可以与throw出现在同一个函数中,也可以不在同一函数中。当throw抛出异常信息后,首先在本函数中寻找与之匹配的catch,如果在本函数中无try-catch结构或找不到与之匹配的catch,就转到其上一层去处理,如果其上一层也无try-catch结构或找不到与之匹配的catch,则再转到更上一层的try-catch结构去处理,也就是说转到离开出现异常最近的try-catch结构去处理。
   (8)在某些情况下,在throw语句中可以不包括表达式,
   (9)如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行。

例2 在函数嵌套的情况下检测异常处理。
这是一个简单的例子,用来说明在try块中有函数嵌套调用的情况下抛出异常和捕捉异常的情况。

#include <iostream>
using namespace std;
int main()
 { void f1();
   try
     
{ f1( ); } //调用fl()
   catch
(double)
     { cout<<"OK0!"<<endl;}
   cout<<"end0"<<endl;
   return 0; }

void f1()
  { void f2();
try
  
{ f2(); } //调用f2()
catch
( char )
  { cout<<"OK1!";}
cout<<"end1"<<endl; }

void f2()
  { void f3();
try
  
{ f3();} //调用f3()
catch
(int)
  { cout<<"Ok2!"<<endl;}
    cout<<"end2"<<endl; }

void f3()
  { double a=0;
try
 
{ throw a;} //抛出double类型异常信息
catch
(float)
  { cout<<"OK3!"<<endl;}
    cout<<"end3"<<endl; }

分3种情况分析运行情况:
   (1)执行上面的程序。图1为有函数嵌套时异常处理示意图。

   (2)如果将f3函数中的catch子句改为catch(double),而程序中其他部分不变,则f3函数中的throw抛出的异常信息立即被门函数中的catch子句捕获(因为抛出的是double型异常,catch要捕捉的也是double型异常,二者匹配)。于是执行catch子句中的复合语句,输出"OK3",再执行catch子句后面的语句,输出"end3"。f3函数执行结束后,流程返回f2函数中调用f3函数处继续往下执行。
程序运行结果如下:
   OK31 (在f3函数中捕获异常)
   end3 (执行f3函数中最后一个语句时的输出)
   end2 (执行f2函数中最后一个语句时的输出)
   endl (执行u函数中最后一个浯句时的输出)
   endO (执行主函数中最后—个语句时的输出)
   (3)如果在此基础上再将f3函数中的catch块改为
      catch(double)
         { cout<<"OK3!"<<endl;throw;}
   f3函数中的catch子句捕获throw抛出的异常信息a,输出"OK3!"表示收到此异常信息,但它立即用"throw;”将a再抛出。由于a是double型,与f2和门函数中的catch都不匹配,因此最后被main函数中的catch子句捕获。程序运行结果如下:
OK3! (在f3函数中捕获异常)
OKO! (在主函数中捕获异常)
endO (执行主函数中最后一个语句时的输出)

三、 在函数声明中进行异常情况指定

   为了便于阅渎程序,使用户在看程序时能够知道所用的函数是否会抛出异常信息以及异常信息可能的类型,C++允许在声明函数时列出可能抛出的异常类型,如可以将例1中第二个程序的第3行改写为:

   double triangle(double,double,double)throw(double);

表示triangle函数只能抛出double类型的异常信息。如果写成

   double triangle(double,double,double)throw(int,double,float,char);

则表示triangle函数可以抛出int,double,float或char类型的异常信息。异常指定是函数声明的一部分,必须同时出现在函数声明和函数定义的首行中,否则在进行函数的另一次声明时,编泽系统会报告“类型不匹配”。
   如果在声明函数时未列出可能抛出的异常类型,则该函数可以抛出任何类型的异常信息。如例1中第2个程序中所表示的那样。

如果想声明一个不能抛出异常的函数,可以写成以下形式:

double triangle(double,double,double) throw(); //throw无参数

   这时即使在函数执行过程中出现了throw语句,实际上也并不执行throw语句,并不抛出任何异常信息,程序将非正常终止。

四、 在异常处理中处理析构函数

   如果在try块(或try块中调用的函数)中定义了类对象,在建立该对象时要调用构造函数。在执行try块(包括在try块中调用其他函数)的过程中如果发生了异常,此时流程立即离开try块(如果是在try块调用的函数中发生异常,则流程首先离开该函数,回到调用它的try块处,然后流程再从try块中跳出转到catch处理块)。这样流程就有可能离开该对象的作用域而转到其他函数,因而应当事先做好结束对象前的请理工作,C++的异常处理机制会在throw抛出异常信息被catch捕获时,对有关的局部对象进行析构(调用类对象的析构函数),析构对象的顺序与构造的顺序相反,然后执行与异常信息匹配的catch块中的语句。

例 在异常处理中处理析构函数。
   
这是一个为说明在异常处理中调用析构函数的示例,为了请晰地表示流程,程序中加入了一些cout语句,输出有关的信息,以便读者对照结果分析程序。

#include <iostream>
#include <string>
using namespace std;
class Student
   { public:
      Student(int n,string nam) //定义构造函数
      
{ cout<<"construtor-"<<n<<endl;
        num=n;name=nam;}
      ~Student(){cout<<"destructor-"<<num<<endl;} //定义析构函数
      
void get_data(); //成员函数声明
     
private:
      int num;
      string name; };

void Student::get_data()       //定义成员函数
  
{ if(num==0) throw num;      //如num=O,抛出int型变量num
    
else cout<<num<<" "<<name<<endl; //若num不等O,输出num,name
    
cout<<"in get_data()"<<endl; //输出信息,表示目前在fet_data函数中
   
}

void fun()                 //过程函数(注意可见性)
  { Student stud1(1101,"tan");//建立对象studl
    
stud1.get_data();       //调用studl的getdata函数
    
Student stud2(0,"Li");   //建立对象stud2
     stud2.get_data();  }     //调用smd2的get data函数

int main()
 { cout<<"main begin"<<endl; //表示主函数开始了
   
cout<<"call fun()"<<endl; //表示调用fun函数
   
try
     {fun();} //调用fun函数
   
catch(int n)
     { cout<<"num="<<n<<",error!"<<endl;} //表示num=0出错
      
cout<<"main end"<<endl; //表示主函数结束
       
return 0; }

分析程序执行过程,程序运行结果如下:

main begiin
call fun()
constructor-110l    //对立Student类对象stud1,num=1101
110l tan          //对象stud1调用Student类的成员函数get_data,num不等O
in get_data()      //执行get_data
constructor-0      //建立Student类对象stud2,num=0,抛出num.
destructor-O       //调用Student类对象stud1的析构函数
destructor-1101    //调用Student类对象stud2的析构函数
num=O,error!     //表示num=0出错
main end         //表示主函数结束

本节简单地介绍了异常处理的机制和使用方法,读者将会在今后的实际应用中进一步掌握它们。

 

 

转自:http://210.44.195.12/cgyy/text/HTML/text/24.htm

 

 

 

 

抱歉!评论已关闭.