《C++程序设计》课件第6章_第1页
《C++程序设计》课件第6章_第2页
《C++程序设计》课件第6章_第3页
《C++程序设计》课件第6章_第4页
《C++程序设计》课件第6章_第5页
已阅读5页,还剩49页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第6章异常和错误6.1异常与bug

6.2异常的体系结构6.3使用异常

6.4调试

本章小结

习题

增强错误恢复能力是提高代码健壮性最有力的途径之一。遗憾的是,在现实中人们往往忽略错误处理情况,就好像程序处在一个无错的状态下进行工作。检查错误是一个乏味的工作,而且会导致代码膨胀。异常处理是C++具有的一项特点,它能够使程序中断并且处理异常情况(通常是错误),并且是以一种有序的、有条理的、一致的方式来处理。异常处理允许程序中的一个部分去发现并分派错误情况,而另一部分去处理错误。通常是某种类的代码,也许是某个库里的类和函数,能够监测出错误而不需要知道正确的处理策略。同样,另一种类的代码可以处理错误而不需要具有监测错误的功能。C++的异常处理机制为程序员提供了一种处理错误的方式,使他们能在给定的系统结构中,以最自然的方式去处理这些错误。异常机制也使处理错误的复杂性更加清晰可见。6.1异 常 与 bug

在设计软件系统的过程中,处理程序中的错误(bug)和其他反常行为是最困难的部分之一。在大型软件开发中,错误是不可避免的,在设计和实现中,花在测试、查找和修改错误上的开销往往也是最大的。异常就是运行时出现的不正常,例如运行时耗尽了内存或遇到意外的非法输入,要打开的文件已经不存在等。异常存在于程序的正常功能之外,并要求程序立即处理。在设计良好的系统中,异常是程序错误处理的一部分。当程序代码检查到无法处理的问题时,异常处理就特别有用。在这种情况下,监测出问题的那部分需要一种方法把控制权转到可以处理这个问题的那部分程序。错误监测程序还必须指出具体出现了什么问题。

异常处理机制提供程序中错误监测和错误处理部分之间的通信。C++中的异常处理由三方面组成,既throw、try和catch,即抛出异常和捕获异常,并对异常进行处理。如果在程序的代码中出现了异常的情况,也就是说,通过当前语境无法获得足够的信息以决定应该采取什么样的措施,程序员可以创建一个包含错误信息的对象并把它抛出当前语境,通过这种方式将错误信息发送到更大范围的语境中。这种方式被称为“抛出一个异常”,由关键字throw表示。如下所示:

classThrowError{constchar*consterrorData;public:ThrowError(constchar*constmsg=0):errorData(msg){}};voidh(){throwThrowError("exceptionhappened!");}intmain(){h();return0;}

上面的代码中,ThrowError是一个普通的类,它的构造函数接受一个char*类型的变量作为参数。在抛出一个异常时,可以使用任意的类型(包括内置类型),但通常应当为抛出的异常创建特定的类。如果在一个函数内部抛出了异常,这个函数就会因为抛出异常而退出。如果不想因为一个throw而退出函数,可以在函数中试图解决实际产生程序设计问题的地方和可能产生异常的地方设置一个try块。这个块被称为try块的原因是程序需要在这里尝试着调用各种函数。try块只是一个普通的程序块,由关键字try引导:

try{//有可能产生异常的各种函数调用

}

使用异常处理时,可以将所有工作放入try块中,然后在try块的后面处理可能产生的异常。这样一来,代码将更容易编写和阅读,因为代码的设计目标不会被错误处理所干扰。当异常被抛出后就会在异常处理器(exceptionhandler)的地方终止,也就是在这个地方异常被处理。程序员需要为每一种要被捕获的异常设置一个异常处理器。异常处理器紧接着try块,由关键字catch标识:

try{//可产生异常的代码

}catch(type1lx1){//处理type1类型的异常}catch(type2lx2){//处理type2类型的异常

}//…catch中的参数也可以省略,异常类型通常提供了对其进行处理的足够的信息。如果想捕获所有类型的异常,用省略号代替异常处理器的参数列表就能实现这一点:

catch(…){//处理所有捕获的异常

}

由于省略号异常处理器能够捕获任何类型的异常,因此最好将它放在异常处理器列表的最后,从而避免它后面的处处理器接受不到任何的异常。由于省略号异常处理器不接受任何参数,因此得不到任何有关异常的信息,也无法知道异常的类型,它是一个全能捕获者。异常处理器必须紧跟在try块之后,一旦某个异常被抛出,异常处理机制将会依次按照在原代码中出现的顺序查找最近的异常处理器,一旦找到匹配的异常处理器,就认为该异常已经被处理了而不再继续查找下去。这就是异常匹配。捕捉异常与函数调用相似,捕捉异常需要传入exception参数,而函数调用也需要传入参数。但异常的参数与函数参数有很大区别:异常的抛出者在抛出异常之后不再拥有程序控制权,异常处理返回之后不再回到抛出地点,而函数调用则能在函数体执行之后获得返回参数继续执行。

异常捕捉不能进行隐式类型转换,例如抛出一个int类型的异常不能被double类型的catch捕捉到。只有在继承关系的异常对象被抛出时,捕捉其父类对象的catch才能够捕捉该异常。异常的参数传递总是通过拷贝传递的。异常抛出者抛出的异常对象首先被拷贝到一个临时对象中,这个临时对象随后被传给异常处理参数。因此,如果通过值的方式获取异常,会发生两次拷贝;通过传引用的方式会发生一次拷贝;而传指针的方式则是应该避免使用的。因为指针指向的对象定义在异常抛出者体内,转到异常处理体时,异常抛出体作用结束,对象被销毁,因此在异常处理中得到的对象指针指向的是一个已经被销毁的对象。

传值方式的异常存在另一个问题:抛出的异常对象如果被一个捕捉父类对象的catch捕捉到,其拷贝仅仅拷贝父类的数据,在异常处理体中,无法根据多态性动态调用原始对象类的方法。基于以上理由,C++中最好使用传引用的异常传递方式。有时如果想把异常推到调用链的更高层次上去处理,可以重新抛出异常。在这种情况下,省略号异常处理器正好符合这种要求。这种处理方法可以捕获所有异常,清理相关资源,然后重新抛出该异常,以使得其他地方的异常处理器能够处理该异常。在一个异常处理器内部,使用不带参数的throw语句可以重新抛出异常:catch(…){cout<<"anexceptionwasthrown"<<endl;//释放资源

throw;

}6.2异常的体系结构标准C++库定义了自己的异常类,为了方便快捷,读者可以使用标准C++的异常类,或从它们继承来派生出自己的异常类。所有的标准异常类都是从exception类派生的。exception类的两个主要派生类是logic_error和runtime_error,这两个类在头文件<stdexcept>中定义。logic_error类用于描述程序中出现的逻辑错误,例如传递无效的参数。runtime_error是指运行时发生的错误,这些错误由无法预料的事件所造成,例如硬件故障或内存耗尽。标准异常类的继承关系如表6.1所示。表6.1标准异常类继承关系

可以把多个异常组成族系。构成异常族系的一些示例有数学错误异常族系和文件处理错误异常族系。在C++代码中把异常组在一起有两种方式:异常枚举族系和异常派生层次结构。程序6.1是一个异常枚举族系的例子。

【程序6.1】enumFileErrors{nonExist,wrongFormat,diskSeekError...};intf(){ try { //... throwwrongFormat; }catch(FileErrorsfe) { switch(fe) { casenonExist: //... casewrongFormat: //... casediskSeekError: //… } } //...}

在try块中有一个throw,它抛掷一个FileErrors枚举中的常量。这个抛掷可被catch(FileErrors)块捕获到,接着后者执行一个switch,对照情况列表,匹配捕获到的枚举常量值。上面的异常族系也可按异常派生层次结构来实现,如程序6.2所示。

【程序6.2】classFileErrors{};classNonExist:publicFileErrors{};classWrongFormat:publicFileErrors{};classDiskSeekError:publicFileErrors{};intf(){ try { //... throwWrongFormat; } catch(NonExist) { //… } catch(DiskSeekError){ //… } catch(FileErrors) { //… } //…}

上面的各异常处理程序块定义了针对类NonExist和DiskSeekError的派生异常类对象,针对FileErrors的异常处理,既捕获FileErrors类对象,也捕获WrongFormat对象。

异常捕获的规则除了前面所说的,必须严格匹配数据类型外,对于类的派生,下列情况也可以捕获异常:

(1)异常处理的数据类型是公有基类,抛掷异常的数据类型是派生类。

(2)异常处理的数据类型是指向公有基类的指针,抛掷异常的数据类型是指向派生类的指针。对于派生层次结构的异常处理,catch块组中的顺序是重要的。因为“catch(基类)”总能够捕获“throw派生类对象”,所以“catch(基类)”块总是放在“catch(派生类)”块的后面,以避免“catch(派生类)”永远不能捕获异常。6.3使用异常程序6.3以一个计算器例子来说明如何在实际的编程中使用异常。

【程序6.3】#include<iostream>#include<cstdlib>#include<cctype>#include<string>usingnamespacestd;//DeclaretheerrorclassclassError{};//CalculatorclassdeclarationclassCalculator{intpos;stringexpress;intaddsub();intmultdiv();intnumber()throw(Error);public:Calculator(){}intCompute(conststring&str)throw(Error);};//Calculatorclassdefinition//以上为"Calculator.h"头文件定义

#include"Calculator.h“intCalculator::Compute(conststring&str)throw(Error){ intretn=0; try { pos=0; express=str; retn=addsub();if(pos<express.length()&&express[pos]!='\0') throwError(); } catch(Error) { cout<<'\r'; while(pos--) cout<<''; cout<<"syntaxerror"<<endl<<'\a'; throw; } returnretn;}intCalculator::addsub(){ intretn=multdiv(); while(express[pos]=='+'||express[pos]=='-') { intop=express[pos++]; intopr2=multdiv(); if(op=='+') retn+=opr2; else retn-=opr2; } returnretn;}//Highestprecedence:multiply/divide.intCalculator::multdiv(){ intretn=number(); while(express[pos]=='*'||express[pos]=='/') { intop=express[pos++]; intopr2=number(); if(op=='*') retn*=opr2; else retn/=opr2; } returnretn;}intCalculator::number()throw(Error){ intretn;

if(express[pos]=='(') { pos++; retn=addsub(); if(express[pos++]!=')')//Musthave')' throwError(); }else{ //Extractthenumber. if(!isdigit(express[pos])) throwError(); charans[80]="0"; inti=0; while(isdigit(express[pos])&&pos<express.length()) ans[i++]=express[pos++]; ans[i]='\0'; retn=atoi(ans); }returnretn;}intmain(){ intanswer; do{ //Readanexpression cout<<"Enterexpression(0toquit):"<<endl; stringexpress; cin>>express;try { Calculatorcalc; answer=calc.Compute(express); if(answer!=0) cout<<answer<<endl; } catch(Error) { cout<<"Tryagain"<<endl; answer=1; } }while(answer!=0); return0;}

运行示例结果如下:

程序6.3中,main()函数从控制台读取表达式,插入一个try块中,并且实例化一个计算器对象。程序调用计算器对象的Compute()函数,把字符串表达式作为一个参数来进行传递。如果一切顺利,Compute()函数将返回一个整数值,即表达式的结果。如果数值为0,程序将会终止;否则,程序将显示这个数值并且请求下一个要计算的表达式。如果在计算过程中有错误发生,计算器对象就会发出一个Error异常。main()函数将捕获这个异常,显示“重试”消息,并请求另一个表达式。Compute()成员函数初始化下标数据成员,到表达式字符串中。插入一个try块中,并且调用位于递归下降顶端的addsub()成员函数。如果上述过程的任何一个步骤碰到了表达式中存在的错误,函数就会发出一个Error异常,并由Compute()成员函数所捕获。当Compute()捕获到一个异常信息时,就使用当前下标数据成员来显示错误是在表达式的何处被发现的。然后,Compute()函数重新发出异常,这样main()函数也可以捕获它了。同样,如果没有任何异常发出,Compute()函数会返回addsub()成员函数所返回的数值。6.4调试一个合格的程序员必须具有很好的调试程序的技能,调试技能的获得要靠大量的编程经验。初学编程的人都有很深的体会,有时发现程序中的错误是很难的,所以调试经验的积累,其重要性甚至超过学习一门语言。不会调试的程序员就意味着他即使会一门语言,也不能编制出好的软件。调试通常离不开编程工具的支持,即与集成开发环境密切相关。下面以VC++开发环境为例来介绍如何进行调试。1.设置环境首先是对编译器环境的设置。为了调试一个程序,首先必须使程序中包含调试信息。一般情况下,一个从AppWizard创建的工程中包含的DebugConfiguration自动包含调试信息,但是Debug版本并不是程序包含调试信息的决定因素,程序设计者可以在任意的Configuration中增加调试信息,包括Release版本。为了增加调试信息,可以按照下述步骤进行:打开Projectsettings对话框(可以通过快捷键Alt+F7打开,也可以通过IDE菜单Project/Settings打开),选择C/C++页,在Category中选择general,则出现一个DebugInfo下拉列表框,可供选择的调试信息显示方式包括:●命令行Projectsettings说明。●无None没有调试信息。● /ZdLineNumbersOnly:目标文件或者可执行文件中只包含全局和导出符号以及代码行信息,不包含符号调试信息。● /Z7C7.0-Compatible:目标文件或者可执行文件中包含行号和所有符号调试信息,包括变量名及类型、函数及原型等。● /ZiProgramDatabase:创建一个程序库(PDB),包括类型信息和符号调试信息。● /ZIProgramDatabaseforEditandContinue:除了前面/Zi的功能外,这个选项允许对代码进行调试过程中的修改和继续执行。这个选项同时使#pragma设置的优化功能无效。选择Link页,选中复选框“GenerateDebugInfo”,这个选项将使连接器把调试信息写进可执行文件和DLL。如果C/C++页中设置了ProgramDatabase以上的选项,则可以选择Linkincrementally。选中这个选项,将使程序可以在上一次编译的基础上被编译(即增量编译),而不必每次都从头开始编译。2.设置断点调试的最基本的方法就是设置断点。断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。

(1)设置断点:可以通过下述方法设置一个断点。首先把光标移动到需要设置断点的代码行上,然后按快捷键F9弹出Breakpoints对话框,方法是按快捷键Ctrl+B或Alt+F9,或者通过菜单Edit/Breakpoints打开。打开后点击Breakat编辑框右侧的箭头,选择合适的位置信息。一般情况下,直接选择linexxx就足够了。如果想设置不是当前位置的断点,可以选择Advanced,然后填写函数、行号和可执行文件信息。(2)去掉断点:把光标移动到给定断点所在的行,再次按F9键就可以取消断点。同前面所述,打开Breakpoints对话框后,也可以按照界面提示去掉断点。条件断点:可以为断点设置一个条件,这样的断点称为条件断点。对于新加的断点,可以单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就被中断。

(3)数据断点:数据断点只能在Breakpoints对话框中设置。选择“Data”页,就可弹出设置数据断点的对话框。在编辑框中输入一个表达式,当这个表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成。例如,在编辑框中输入g_bFlag这个全局变量的名字,那么当程序中有g_bFlag=!g_bFlag时,程序就将停在这个语句处。(4)消息断点:VC也支持对Windows消息进行截获,截获方式有两种:窗口消息处理函数和特定消息中断。在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在Breakpoints对话框中写入消息处理函数名,那么每次消息被这个函数处理,断点就到达(笔者觉得如果采用普通断点在这个函数中截获,效果应该一样)。如果在下拉列表框中选择一个消息,则每次这种消息到达,程序就中断。

3.Watch监视

VC支持查看变量、表达式和内存的值。所有这些观察都必须是在断点中断的情况下进行。观看变量的值最简单,当断点到达时,把光标移到这个变量上,停留一会就可以看到变量的值。VC提供一种被称为Watch的机制来观看变量和表达式的值。在断点状态下,在变量上单击右键,选择QuickWatch,就弹出一个对话框,显示这个变量的值。单击Debug工具条上的Watch按钮,就出现一个Watch视图(Watch1,Watch2,Watch3,Watch4),在该视图中输入变量或者表达式,就可以观察它们的值。注意:这个表达式不能有副作用,例如++运算符绝对禁止用于这个表达式中,因为这个运算符将修改变量的值,导致软件的逻辑被破坏。

Watch监视有以下三种:

1)Memory(内存)监视由于指针指向的是数组,Watch只能显示第一个元素的值。为了显示数组的后续内容,或者要显示一片内存的内容,可以使用Memory功能。在Debug工具条上点击Memory按钮将弹出一个对话框,在其中输入地址,就可以显示该地址指向的内存的内容。

2) Varibles(变量)监视单击Debug工具条上的Varibles按钮,将弹出一个对话框,显示所有当前执行上下文中可见的变量的值。特别是当前指令涉及的变量将以红色显示。

3)寄存器监视单击Debug工具条上的Reigsters按钮,将弹出一个对话框,显示当前所有寄存器的值。4.进程控制

VC允许被中断的程序继续运行、单步运行和运行到指定光标处,分别对应快捷键F5、F10/F11和Ctrl+F10。各个快捷键的功能如下:

F5:继续运行。

F10:单步,如果涉及到子函数,不会进入子函数内部。

F11:单步,如果涉及到子函数,将进入子函数内部。

Ctrl+F10:运行到当前光标处。

5.调用堆栈调用堆栈(CallStack)反映了当前断点处函数是被哪些函数按照什么顺序调用的。单击Debug工具条上的CallStack按钮,将显示CallStack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中。

6.其他调试手段系统提供一系列特殊的函数或者宏来处理Debug版本相关的信息,如下:

TRACE:使用方法和printf完全一致,它在output框中输出调试信息。

ASSERT:它接收一个表达式,如果这个表达式为TRUE,则无动作;否则,中断当前程序的执行。对于系统中出现这个宏导致的中断,应该认为你的函数调用未能满足系统调用此函数的前提条件。例如,对于一个还没有创建的窗口调用SetWindowText等。VERIFY和ASSERT功能类似,所不同的是,在Release版本中,ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值。一个好的程序员不应该把所有的判断交给编译器和调试器,应该在程序中自己加以程序保护和错误定位,具体措施包括:

(1)对于所有有返回值的函数,都应该检查返回值,除非你确信这个函数调用绝对不会出错,或者不关心它是否出错。

(2)一些函数返回错误,需要用其他函数获得错误的具体信息。例如accept返回INVALID_SOCKET表示accept失败,为了查明具体的失败原因,应该立刻用WSAGetLastError获得错误码,并针对性地解决问题。(3)有些函数通过异常机制抛出错误,应该用TRY-CATCH语句来检查错误。程序员对于能处理的错误,应该自己在底层处理;对于不能处理的,应该报告给用户让他们决定怎么处理。如果程序出了异常,却不对返回值和其他机制返回的错误信息进行判断,只会加大找错误的难度。另外,VC中要编制程序不应该一开始就写cpp/h文件,而应该首先创建一个合适的工程。因为只有这样,VC才能选择合适的编译、连接选项。对于加入到工程中的cpp文件,应该检查是否在第一行显式地包含stdafx.h头文件,这是MicrosoftVisualStudio为了加快编译速度而设置的预编译头文件。在这这个#include“stdafx.h”行前面的所有代码将被忽略,所以其他头文件应该在这一行后面被包含。对于.c文件,由于不能包含stdafx.h,因此可以通过Projectsettings把它的预编译头设置为“不使用”,方法是:首先打开Projectsettings对话框,然后选择C/C++,接着在Category中选择PrecompilationHeader,最后选择不使用预编译头。本章小结为了检测异常,程序中使用try、catch和throw语句。异常处理使程序中错误的检测简单化,并提高了程序处理错误的能力。编程技巧与注意事项:

(1)所谓异常是指程序中有运行错误,程序应能检测错误;

(2) try语句使C++能够进行异常检测;

(3) catch紧跟在try语句后面,以捕获异常;

(4)通过程序中的throw语句报告异常;

(5)异常通过抛出一个类型和catch的参数相匹配而捕获;(6)捕获异常后,程序将执行catch中的语句;

(7)如果程序抛出一个不能捕获的异常,C++将执行默认异常处理函数;

(8)调试是编程中非常重要的一项技能,找到程序中的错

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论