《零点起飞学C++之有错也不怕——错误与.pptx》由会员分享,可在线阅读,更多相关《零点起飞学C++之有错也不怕——错误与.pptx(63页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。
1、第23章 有错也不怕错误与调试当程序没有按照程序员的预期执行或意外中断时,就产生了错误。调试就是查找错误发生点、错误原因的过程。程序错误是编程实践中常见的问题,既使最有经验的程序员也不能保证一次就编写出没有任何错误的程序。编写程序的大部分时间实际上就是在进行调试,本章将向读者详细讲解错误和错误的调试,通过本章的学习,读者可以识别程序中的错误,并学会调试和修改这些错误的能力,从而编写出高质量的代码。23.1 什么是错误程序错误常直接写做英文的bug,也称为缺陷、臭虫。它指在软件运行中因为程序本身有错误而造成的功能不正常、死机、数据丢失、非正常中断等现象。将程序错误称为bug是由于计算机发展早期的
2、一件事情。二战期间,哈珀中尉带领他的小组制造了一台称为“马克二型”的计算机。一天,马克二型突然死机了。技术人员试了很多办法,最后定位到第70号继电器出错。哈珀观察这个出错的继电器,发现一只飞蛾躺在中间,已经被继电器打死。他小心地用镊子将蛾子夹出来,用透明胶布贴到“事件记录本”中,并注明“第一个发现虫子的实例”。此后,计算机界就习惯将计算机的各种非正常事件都称为bug。按照错误的发生时间不同,可以将错误分成语法错误、链接错误和运行时错误。其中,语法错误发生在编写代码时,也叫编译时错误。链接错误发生在将程序代码链接为可执行程序的过程中。运行时错误则发生在运行程序的过程中。3种错误的排查和解决难度依
3、次为语法错误、链接错误和运行时错误。有时,程序虽然没有发生上述3种错误,但是运行结果却不是预期的,这时就发生了逻辑错误。逻辑错误往往是算法设计错误造成的,本章主要讨论前3种错误类型。23.2 错误的种类为了处理错误,首先要搞清楚错误的内容和发生时机。这些错误大多发生在编写源代码,并将源代码转换成可执行程序的过程中。下面详细讲解几种常见的错误。23.2.1 语法错误语法错误发生在编写源代码时,指程序中包含了违反语法规则的语句。这类错误在编译时由编译程序自动发现。这类错误很普遍,是初学者最容易犯的错误;也很顽固,即使是最有经验的程序员也难以保证不犯这类错误。【示例23-1】从命令行接受数据并输出。
4、该程序在编译时会输出如下内容。分析:从输出可以看出,该示例共有4个错误。其中,第1个错误是第5行语句中的关键字end书写错误引起的,正确的写法应该是endl。第2个错误是第6行错用了“”。第3个错误就需要仔细理解了。从输出结果看出错误发生在第8行,错误内容中却说是data前缺少“;”号。这说明是第7行缺少“;”号,导致两行连在了一起,但是编译器发现这两行是两条独立的语句,所以提示8行有错。第4个错误是8行括号不全引起的,应该在“3”后补一个“)”。所有程序员终生都要受语法错误的困扰,即使再小心仔细也难以避免。但是语法错误也是最容易处理和发现的。只要按照提示找到错误点,根据错误内容修改即可。这类
5、错误只能随着经验的积累逐步的减少,却不能根除。有时编译器还会对某些语句给出警告,例如,将浮点数赋给整型数,将很小的数作为被除数等。这些不是错误,但却潜在地存在问题,也是需要高度重视的。23.2.2 链接错误链接错误发生在程序链接时。将编译过的程序与程序使用的库链接生成可执行程序时,如果不能在所有的库和目标文件内找到所引用的函数、变量或标识符,就会产生链接错误。这常常是符号不存在、拼写不正确或者使用错误引起的。【示例23-2】链接错误的示例代码。该程序编译链接时,会有如下输出。分析:从输出可以看出示例代码除了仅有一个数据转换警告外,已经通过了编译,可以不管这个警告。下面可以看出有3个链接错误。第
6、1个是提示函数fun()找不到。这是因为声明的fun()函数的参数为浮点型,但定义时却错写成了整型,所以链接程序认为找不到参数为浮点型的fun()函数。第2个错误是声明了外部变量x,但是却没有找到定义它的库文件。第3个是提示有两个无法找到的外部引用,所以导致不能链接为可执行程序。一般来讲,可以将链接错误分为工程内链接错误和工程外链接错误两种。其中,工程内链接错误指工程内使用的对象在链接时未能找到,是代码级别的。工程外链接错误指工程使用的外部对象未能找到,一般是使用外部编译好的库造成的。下面分别予以说明。1工程内链接错误这包括函数或变量不存在和函数或变量所在的文件没有被正确编译两种。其中,前者发
7、生的原因是由于函数和变量只声明未定义,函数声明和定义的参数列表不一致,或者拼写错误等。后者是由于函数和变量所在的文件没有加到工程中,预处理宏或条件编译导致函数或变量没有被正确编译等。2工程外链接错误这包括链接的函数或变量没有被正确导入,找不到链接的库文件,调用方式错误等。注意:如果程序中用到了外部库或头文件,则需要在“toolsoptions”的directories页增加路径,以避免链接错误。23.2.3 运行时错误当程序链接完后就生成了可执行文件,这时就可开始程序的执行。虽然已经排除了编译时错误和链接错误,但是在运行时仍然可能发生各种各样的错误。这种运行时发生的错误就叫运行时错误,它多是因
8、为发生了计算机不能处理的事件,而导致程序不正常结束,有时也叫异常。【示例23-3】下述代码存在指针相关的运行时错误。分析:该示例在运行时将会发生错误,这种错误在编译和链接时不会发现。指针p在释放后再次被使用,这导致了内存访问错误。此外,由于变量x要在运行时从命令行获得输入,所以语句“*p=*p/x”存在发生被除数为0的运行时错误。错误发生时的画面如图23-1所示。图23-1 运行时错误运行时错误是程序执行期间发生的错误,它不同于编译期间发生的错误。运行时错误可能是程序中的bug引起的,也可能程序并无错误,例如机器存储器不够引起的。运行时错误主要包括以下几类:硬件检测的错误,例如示例23-3中的
9、内存错误和被0除错误;系统错误,例如文件操作失败错误;逻辑错误,例如数组越界错误;其他错误,例如输入数据格式错误。运行时错误经常会发生,但是一个好的软件应该尽可能避免这种错误,或者是做好从这种错误中恢复的预案。当发生这种错误时,程序往往会立即终止并退出,这就会导致申请的内存没有释放、打开的文件没有关闭等问题发生。运行时错误不像语法错误和链接错误那样,编译程序和链接程序会定位到具体的语句行上。但是开发环境一般都会提供带有单步执行的调试功能,可以方便地跟踪程序,查找出错的语句。23.3 排查错误错误的排查指发现并解决错误。错误的发现就是要找出错误的原因和发生错误的语句。由于常将错误称为bug,所以
10、错误的发现也被称为debug。错误的解决是在发现错误之后,通过分析错误的原因,纠正错误的语句。错误的解决要依赖于实际的程序和程序员,本节只讲常见的错误发现方法。23.3.1 看懂错误信息当发生错误时,编译器会给出一些提示,根据这些提示就可以查找并定位到错误发生点。下面就来分析一下示例23-2的错误提示。最后一行显示“test23_2.exe-3 error(s),0 warning(s)”,表明有三个错误和零个警告。第一条错误的内容是:test23_2.obj:error LNK2001:unresolved external symbol int _cdecl fun(float)(?fun
11、YAHMZ)首先,test23_2.obj表明错误发生在test23_2.obj目标文件中。其后的error LNK2001是错误号,通过该错误号可以查询具体的错误信息。后面给出了错误原因,这里是说“外部符号int _cdecl fun(float)找不到”。最后括号里的内容是该函数的入口点,一般不需要去管它。23.3.2 错误发现的常见方法有些错误从错误提示就可看清楚原因,这类错误多是编译、链接时的硬错误。对于程序中的逻辑错误、堆栈溢出、算法错误等,有时很难发现错误发生点。即使错误提示给出了错误的发生点,有时也并不是真正的位置和原因,这时就需要对程序进行调试来查找并解决问题。调试一般有以下3
12、种方法。1调试器首先是使用专业的调试工具,如gdb、dbx、valgrind等专门的调试工具。其次是使用IDE集成的调试工具。这些工具都是通过设置断点来跟踪程序。当运行到断点处时,程序终止,然后就可以“单步”、“跳过”、“进入”等多种方式跟踪程序的执行。2加输出语句有些时候,使用调试器的调试过程比较慢,也较难看出直观的结果。这时可以考虑自行在程序内加入输出语句,将有疑问的变量值输出。通过与预期的结果比较,观察这些值的变化,从而定位错误发生点并找出错误。在Visual C+中,如果选择支持MFC,还可以使用TRACE宏来输出内容。该宏是将内容输出到debug窗口中,而且只有在debug状态下才能
13、起作用。3断言用户还可以使用断言来判断某个值是否是0,有assert、ASSERT、VERIFY这3种。其中assert是标准C+中的宏,ASSERT和VERIFY是MFC中的宏。用被测试的变量或表达式作为它们的参数,如果参数为0就会弹出错误窗口。说明:从广义上来讲,程序错误并不表示程序一定就存在问题,只是说程序没有按程序员的预期去执行。例如,如果算法设计有误,或者程序逻辑错误,那么就程序本身来说没有任何问题,它忠实地按照代码的安排执行了。但是计算的结果却不是预期的。23.3.3 如何调试调试就是理解系统的行为并调整到需要的状态的过程。一般来讲,调试时需要遵循以下3个原则。调试的原则之一就是要
14、理解系统的行为。这就是说,程序员要首先理解系统在干什么,而不是想当然地认为系统应该怎么样。如果不理解系统,就不能指望让系统完成预期的工作。因为任何一个改动都有可能影响到其他的方面,这样有可能解决了旧的bug,却带来了新的bug。调试的原则之二是对程序的任何改动都要小心谨慎,避免引入新的问题。对程序的任何改动都有引入新问题的危险性。如果解决了一个问题,又引入了一个或多个新的问题,那么就得不偿失。但是这是难以避免的,只要加入新的未经测试过的代码就有可能导致新的问题存在。基于这个原因,也建议程序员在编程时尽量采用封装技术。这样当被封装的对象测试无误后,就可以重复利用,而不必担心程序的调试会影响到对象
15、内部。原则之三是要保持版本的跟踪,即每次改动都最好保留一份备份版本。因为,并不能保证每次的程序改动都是正确无误的。有时改动后新引入的问题比原来的问题更棘手,更难解决。如果有一份先前的版本就可以还原,或者说撤销本次的改动,这样就可以避免花费不必要的精力去解决新的问题。23.4 常见bug的分类语法错误比较容易识别,这一节主要介绍一下其他的非语法类常见错误。1内存泄露内存泄露指分配的内存在用完后没有收回,导致一段时间后内存减少,系统变慢。例如,malloc、new等动态申请内存的操作。当申请了一块内存然后在释放前又一次申请时,那么原来那块内存将丢失不能被收回。2逻辑错误当语法正确,但是却没有达到预
16、期的目的时,就发生了逻辑错误。这多是算法设计有缺陷或代码输入错误造成的。这种错误无法由编译器和调试器发现,因为程序没有问题,并如实按照要求去执行了。3内存越界当指针或数组使用了不属于自己的内存时,就会引发该错误。当这种错误仅导致访问到了不该访问的数据时,未必会引起程序中断。但是当访问到系统区域时,则多数情况下会导致程序崩溃。4无限循环对于循环类语句,如for、while等,如果循环终止条件设置不恰当,就会导致程序无限循环下去。与第2种错误一样,程序也没有问题,但是执行时却不会停止。5指针错误指针导致的错误主要有3类:没有初始化,却使用;已删除,但仍然使用;指针无效。前2类都可以通过观察源代码来
17、排查,第3类则必须通过分析来找出原因。例如,强行将字符型指针转换为整型指针,系统只是对内存块的边界作了重新界定,但是对于数据没有任何操作。由于内存块扩大了,转换后指针的数据将是没有任何意义且未知的。6编码错误这主要指声明与定义不一致,或者使用与定义不一致。例如,声明某个函数接收3个参数,但是定义时却只有2个,或者使用时只给出了2个等。具体的编程实践中会出现各种各样的错误,上面仅列出了常见的几类。23.5 调试的窍门相关研究表明软件的编写中,大多数时间和精力是花在了调试上。好的调试方法也是编写好程序的关键。本节将介绍常用的几种调试技巧,主要有断言、轨迹、断点等。23.5.1 使用断言assert
18、断言就是判断,assert断言有两种,分别是assert和ASSERT。其中,前者是标准C+中的宏,后者是MFC中的宏。断言的功能是测试它的参数,若参数为0,则中断执行并打印一段说明消息。在Release 版本的程序中它不起任何作用。assert和ASSERT的使用方法一样,下面以assert断言来说明断言的使用方法。【示例23-4】演示assert断言使用的方法。分析:当从命令行输入2时,导致assert的参数为0,将会显示图23-2所示的界面。图23-2 assert断言的输出说明:ASSERT宏同assert使用方法一样,但需要包含afx.h头文件,afx.h是MFC的头文件。后面章节中
19、的VERIFY和TRACE宏都需要MFC的支持。23.5.2 使用断言verifyverify也是一种断言,不仅可以断言简单变量,还可以断言含有函数的表达式。它是MFC中的宏,使用时写做VERIFY。【示例23-5】演示VERIFY断言的使用方法。分析:当从命令行输入0时,导致VERIFY的参数为0,将会显示图23-3所示的画面。图23-3 VERIFY断言的输出提示:LINK:fatal error LNK1104:cannot open file nafxcwd.lib 编译时出现此问题,说明将项目设置设为了“在静态库中使用MFC”。Microsoft Visual C+6.0标准版不支持
20、静态链接到MFC库。解决方法为使用动态链接到MFC库。具体操作为:1.打开您的 Project。2.从 Project 菜单中单击 Settings。3.在 Setting for,选择 All configurations。4.单击 General 选项卡。如果它是不可见的使用选项卡滚动按钮向左滚动。5.在 Microsoft foundation classes 组合框选择 Use MFC in a Shared DLL。6.单击 Ok 以保存所做的更改。23.5.3 assert VS verifyASSERT与VERIFY宏在Debug模式下作用基本一致,二者都对表达式的值进行计算,如
21、果值为非0,则什么事也不做;如果值为0,则输出诊断信息。但是在Release模式下效果完全不一样。ASSERT不计算表达式的值,也不会输出诊断信息;VERIFY计算表达式的值,但不管值为0还是非0都不会输出诊断信息。VERIFY与ASSERT用在程序调试上并无本质上的区别,但是建议尽量使用ASSERT宏。因为VERIFY在release版本中虽然不输出,但仍然要做计算,这会浪费不必要的CPU时间。23.5.4 轨迹跟踪轨迹跟踪就是人为地加入输出语句,可以是输出变量的值,也可以是输出特定的记号。经过这样处理的程序在运行时会输出一系列的轨迹,通过观察这些输出就可以分析出程序运行了哪些语句,在哪里出
22、了问题。这种方法有时比使用调试器要轻便和快捷许多。【示例23-6】在示例23-3中加入输出语句,用轨迹法跟踪语句的执行。运行后的输出如图23-4所示。图23-4 轨迹跟踪的输出分析:输出中带*的数字是程序中加入的输出语句,不带*的是命令行输入的语句。这些带*号数字的输出勾画出了程序的运行轨迹。用户从这些轨迹可以看出程序运行到哪里了,以及运行了那些语句。从图23-4中可以看出,在输出“*7”后,程序发生运行时错误,而预期的“*8”没有输出。这表明错误发生在这两条输出语句中间,这样就很容易地定位到了语句“*p=12”上,该语句出错是因为使用了已经销毁的指针造成的。如果创建工程时选择了MFC支持,还
23、可以使用TRACE宏。该宏与上述方法的区别在于它是将输出内容送到debug窗口中,而且只有在debug状态下才起作用。启动debug状态的方法是:依次选择菜单【Build】,菜单项【start debug】、【go】即可。工程创建提示:选择win32 Console Application,按下【OK】按钮,然后选择An application that supports MFC。打开test23_7.cpp添加相应代码。【示例23-7】使用TRACE宏来跟踪语句的执行。分析:该示例运行时,将会在debug窗口内看到TRACE的输出。这种轨迹跟踪方法的好处是它只在debug状态下才起作用,因此
24、在release版本中不需要将这些语句删除,而前一种跟踪方法则需要删除输出语句。23.5.5 使用断点断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。断点有条件断点、数据断点、消息断点3种。条件断点就是带有条件判断的断点。只有当满足该条件时,断点才能中断,否则会继续执行下一条语句。数据断点是对某个表达式进行监视,当表达式的值发生改变时,就发生中断,进入跟踪状态。一般情况下,这个表达式应该由运算符和全局变量构成。消息断点VC也支持对Windows消息进行截获,即当某个特定消息发生时
25、产生中断,进入跟踪状态。23.6 使用交互式调试Visual C+编程环境给程序员提供了方便且强大的调试环境。在该环境下,可以设置断点、观察变量的内容、跟踪程序的执行等,本节就向读者详细讲解它的调试功能。23.6.1 设置和删除断点设置断点时,首先把光标移动到需要设置断点的代码行上,然后通过下述两种方法来设置:按F9键直接设置断点;选择Edit|Breakpoints命令,打开Breakpoints对话框。单击Break at编辑框的右侧箭头,选择合适的位置信息。下面是在Breakpoints(断点)对话框中设置三类断点的方法。1条件断点在location页,当选择好断点位置后,单击condi
26、tion按钮,在弹出的对话框中输入断点条件。例如,输入“x10”,则当x的值为负数或大于10时将发生中断。2数据断点首先设置普通断点,然后进行调试。当程序停在预设的断点处时,在Breakpoints对话框中的Data页内输入被监视的变量名,然后按F5键继续运行。当该变量的值发生改变时程序就在此中断,进入跟踪态。注意:数据断点只能在Breakpoints对话框中设置。选择Data页,就显示了设置数据断点的对话框。在文本框中输入一个表达式,当这个表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成。3消息断点Visual C+也支持对Windows消息进行截获,只
27、能在Breakpoints对话框中设置。选择Message页,选择要截获的消息以及消息发生的函数。当在该函数内收到该消息时,就会 中断。去掉断点时,只需把光标移动到给定断点所在的行,再次按下F9键就可以取消断点。也可以打开Breakpoints对话框后,按照界面提示去掉断点。23.6.2 使用Debug窗口当进入调试状态时,有以下几个辅助工具可供使用。变量观察面板(Watch):在观察窗口中,加入特定的变量,就可以监控它的取值变化。如图23-5就是在Watch面板内观察变量szName1的画面。图23-5 Watch对话框快速查看对话框(QuickWatch):功能和观察面板差不多,当输入变量
28、时,立刻就会显示变量的内容。如图23-6所示为QuickWatch对话框。图23-6 QuickWatch对话框变量面板(Variables):变量面板有3个标签,如图23-7所示。Auto标签显示了当前语句和前一条语句用到的变量,Locals标签显示当前函数的局部变量,this标签显示了this指针执行的对象。图23-7 Variables面板寄存器面板(Register):该面板能够监控CPU的寄存器、标志值连同浮点堆栈,如图23-8所示。图23-8 Register面板内存面板(Memory):该面板可显示从某特定地址开始的虚拟内存,如图23-9所示。用户可以通过Address文本框指定
29、虚拟内存的开始地址。图23-9 Memory面板调用栈面板(Call Stack):该面板显示当前程序执行的一系列函数调用,当前函数在堆栈的顶端。如图23-10所示为Call Stack面板。图23-10 Call Stack面板反汇编面板(Disassembly):该面板提供了查看编译器生成的对应于源代码的汇编指令的功能。23.6.3 使用Watch面板在23.6.2节的7种工具中,Watch(变量观察)面板是程序员可以操作的唯一一个,也是最常用的工具。因为调试程序时,经常需要根据某个变量的取值来判断程序的状态。本节就来详细讲解Watch面板的使用方法。Watch面板的外形如图23-11所示
30、。面板中有name和value这2列,第1列是变量的名字,第2列是变量的值。共有4个标签页,表明可以跟踪4组变量,每个标签页内可以观察多个变量。图23-11 Watch(变量观察)面板新增变量时,双击一个新行,就可以输入变量的名字。如果变量已经被赋值,则右边的value域就会显示变量的内容;否则,会提示符号未发现。如果变量是复合变量(如数组、结构体等),还会在变量名前显示“+”号,点开该加号就会显示该变量的所有分量,如图23-11所示。在图23-11中,szName1是字符数组,点开加号后,就会显示数组的每一个分量。如果不需要再监视某个变量的内容,只需要选中变量所在的行,直接按Del键删除即可
31、。23.6.4 使用步进方式调试在调试程序时,可以选择4种步进方式,如图23-12所示。图23-12中右上方方框内的4个图标分别表示“步入”、“越过”、“步出”和“运行到光标处”。图23-12 Debug(调试)面板步入也叫单步方式,表示程序一次执行一条指令。如果当前语句是函数,就会跟踪进入函数内;否则会移动到下一条语句继续执行。越过表示跨过当前的语句。这并不是说不执行当前语句,而是当前语句为函数时,不进入函数内部,而是一次执行完整个函数。如果当前语句不是函数,则该方式相当于单步执行。步出表示从当前函数跳出到调用它的函数内继续执行下一条语句。当函数的语句比较多时,就可以用该方式将剩下的语句全部执行完,而不必每条都跟踪。运行到光标处是表示放弃跟踪从当前执行处到当前光标处的一段语句。当不需要跟踪接下来的一段代码,而是要跟踪较远处的代码时,可以将光标放到目标处,然后单击该按钮,程序就会马上运行到该处。说明:当前语句不是函数时,越过方式就成了步入方式,两种方式都变成了单步执行方式;如果当前光标在下一条语句处,则运行到光标处与步入方式等价。23.7 小 结本章主要讲解了程序错误的概念和调试的方法。重点和难点是掌握程序查错的方法和手段。第24章将进入本书的应用篇,首先讲解在数据结构中的应用。
限制150内