2023年12月6日发(作者:)

第四篇 在Visual C++ 6中调试

在初学者的思想中,经常把处理程序的语法错误等看作是调试程序。而对非初学者来说,调试程序主要是指处理程序的语义(semantics)错误和运行时的异常处理。其中,语义错误的一种情况指程序代码的语法是正确的,程序也能被编译和链接生成可执行的程序,但由于程序中存在不正确的逻辑因而会在程序运行时产生错误。另一种情况的语义错误指程序没有按设计者的预想工作,从而出现意料之外的结果。而异常处理指程序在运行中遇到特殊情况(如内存不足、要访问的文件不存在)如何进行处理等。本篇主要包括以下内容:

1、

程序调试方法

2、

程序跟踪

3、

交互式调试

4、

C++的异常机制与标准异常处理

5、

学习和提高调试技巧

一、 程序调试方法

当程序在运行时出现错误或者出现意想不到的运行情况时,我们必须通过跟踪某些关键量的变化来确 定出错的原因。根据跟踪方式的不同,程序的调试方法(不是软件的调试)可以分为手动跟踪、程序跟踪和交互式调试三种方法,程序设计者应根据实际情况来选取这些调试方法。

所谓程序的手动跟踪,其实就是通过认真地阅读程序代码,通过画流程图等方法弄清程序运行的流程,同时手动运行和跟踪程序的每一步,看其实际运行结果是否和设计结果一致。这种跟踪方法,对于小型程序或简单函数来说是非常合适的。但对于大型程序来说,这个过程太耗时间了。

所谓程序跟踪,即在程序的关键位置插入跟踪语句(如用printf、cout等语句输出变量的值)以追踪变量值的变化,通过观察程序的运行情况而最终找到程序出错的原因。

交互式调试则是利用集成开发环境中所带的调试器软件,通过在程序中设置断点,同时对所有变量进行自动跟踪,从而最终找出错误原因的方法。

初学者每当遇到运行时错误或者程序出现意料之外的运行结果时,往往茫然不知所措或者惊慌不定。其实应该认识到,程序出现错误之时往往是学习程序设计的最佳时机。通过对错误的处理,使我们能够更深刻更真实的理解语法以及程序设计中应该重视的方 方面面,从而在以后的设计中避免犯同类错误。总之,程序调试是每一位初学者都必须跨过的坎,所有的常用调试技术都应该认真学习和掌握。

此外值得注意的是,程序调试技能的培养与提高往往不是传统的教学方法能够完成的,这往往需要学习者有较高的专注程度,同时通过大量的编码来渐进掌握。不过,虽然程序调试的技巧和能力是应当在实际的编程过程中来掌握的,但在此之前对调试方法和相关知识作一些了解是非常有必要的。由于手动跟踪相对简单,因此以下主要介绍程序跟踪和交互式调试方法。

二、 程序跟踪

一个程序需要调试,往往是由于在程序运行中出现死锁、程序不响应,或者是程序运行结束却输出不正确,或者是程序无缘无故中止却没有任何出错信息或异常。这些情况与编译错误和链接错误不同,编译错误和链接错误分别由编译器和链接器发现,并且一般能大致判定出错的原因和位置,有些甚至能非常准确的定性错误产生的原因,因而能较容易的发现和处理。而调试时的异常情况常常是没有任何系统信息可以帮助你的,对于错误在何处发生,你也可能有线索, 也可能没有线索,取决于你对语言的了解、对程序流程的了然于胸,因此,调试是比处理编译和链接错误int main()

{

int array[5] = {0,1,2,3,4};

array[5] = 5;//下标越界

return 0;

}

更困难的事情,需要更高的技巧和水平。以下是一个最简单的、也是大多数初学者最常犯错误的、需要调试的程序。

这个程序由于数组下标越界,从而出现内存的非法访问系统错误,但这个错误是不会被编译器和链接器发现的,因而能够顺利地通过编译链接从而生成应用程序。但在程序运行时却会出现错误,这个错误一般会被操作系统捕获,捕获后如果系统中没有实时的调试器则会提示关闭程序,如系统中有实时的调试器在则会由调试器接管应用程序。实时调试器一般会提示用户选择直接退出程序或者启动调试,如下图则是在系统中安装有VC6所带实时调试管理器(Debug

Manager)时的情况:

当用户选择[确定]时将由VC启动调试器来调试程 序,如下图所示:

对于需要调试的程序,使用程序本身并不需要的输出语句来进行跟踪,是调试程序的一个最重要的方法。其中最常用的方法就是在程序的关键位置用printf语句或cout语句输出当前关键变量或某些变量的值,通过查看这些变量的值的变化来查找程序出错的原因。当然,如果愿意,你可以在每行源代码的后面都加上输出语句,从而一步一步地追踪程序的行为,但这样作在时间和精力上是缺乏效率的。因此必须经过认真思考和选择,确定应在何处插入输出语句之后,跟踪才可能凑效。

插入printf和cout输出语句的方法存在一个缺点, 就是当程序调试完毕后我们必须将所有程序不需要的输出语句逐一手动删除,如果再次需要调试时又要经历逐一加上和删除的过程,很不方便。为此,在C++中提供了另外的处理方法实现对变量的检查,并且能很方便地让这些检查语句失效,其中最常用的就是assert宏。assert宏接收一个表达式,如果这个表达式为真,则无动作,否则中断当前程序执行(参考并完成课堂实践内容1)。使用assert宏的方便之处在于,在调试完成之后,assert语句还是可以保留在程序中,但它们可以被很有效地“关闭”掉。我们所要作的只是在“#include ”语句前加上“#define

NDEBUG”即可。如果你以后又需要进行调试,并想恢复原来的所有assert宏,只需删除这行宏定义语句即可。与assert宏相对应的一个宏是VERIFY宏,所不同的是,在Release版本中ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值(VERIFY宏是MFC中的宏)。

另一个常用的方便的语句是trace宏,它的使用方法和printf完全一致,能在output框中输出调试信息(参考并完成课堂实践内容2)。

三、 交互式调试

使用集成开发环境所带的调试器来边运行程序边观察和调试是非常方便的。这种调试方法一般首先要在程序中关键位置设置断点,然后开始调试程序。此时可以让程序单步运行,也可以让程序直接运行到光标所在的那行,也可以让程序运行到断点处,然后在程序暂时停止时查看所有变量的值。通过在程序的某行右击鼠标并选择[Insert/Remove Breakpoint]就可以很方便地在此行处加上或去掉断点(位置断点)。也可以通过把光标移动到需要设置断点的代码行上,然后按F9快捷键。另一种方法是按快捷键CTRL+B或ALT+F9,或者通过菜单Edit/Breakpoints打开Breakpoints对话框,之后点击[Break at]编辑框的右侧的箭头,选择合适的位置信息来设置。一般情况下,直接选择line xxx就足够了,如果想设置不是当前位置的断点,可以选择Advanced,然后填写函数、行号和可执行文件信息。如下图所示:

如果调试完程序要去掉断点,可以把光标移动到给定断点所在的行,再次按F9就可以取消断点。也可以在打开Breakpoints对话框后,选择其中的[Remove]或[Remove all]来去掉某个或全部断点。此外,在Breakpoints对话框可以设置的断点也分为条件断点、数据断点和消息断点三类。我们可以为断点设置一个条件,这样的断点称为条件断点。对于位置断点,可以通过单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就 被中断。值得一提的是最后一个设置,它可以让程序先执行多少次后才到达断点。这种情况在循环中特别有效,因为我们常常希望让程序运行到一个临界值状态时才中止它。

数据断点则只能在Breakpoints对话框中设置。选择“Data”页,就显示了设置数据断点的对话框。在编 辑框中输入一个表达式,当这个表达式的值发生变化时,数据断点就到达。消息断点:VC也支持对Windows消息进行截获。他有两种方式进行截获:窗口消息处理函数和特定消息中断。

在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在Breakpoints对话框中写入消息处理函数的名字,那么每次消息被这个函数处理,断点就到达。

此外,在交互式调试中,我们还可很方便地在程序运行时“即时”地改变变量的值,甚至“即时”地改变寄存器或内存中的值。例如你可以在Variable窗口中直接又击你想改变的变量值并键入你想使有的值再回车即可,当程序恢复运行时这个变量的值就是你输入的值而不是以前的值了(完成课堂实践内容3)。

四、 C++的异常机制与标准异常处理

由于我们的程序在运行中可能出现异常,而在现代操作系统中一般都有对异常进行相应处理的机制,即当某些应用程序因错误导致如死锁、不响应或引起系统资源(如内存不足或CPU被占用)不足时操作系统会根据异常的类型作相应的处理。因此,要更好地掌握调试技巧,有必要对异常处理机制有一定程度的 了解。

C++中的异常处理机制由try/throw/catch三个部分//例一

#include

#include

#include

void main( void )

{//throw 2;

/*如果在try块外抛出异常则不会被本程序中的catch块捕获

而会被操作系统捕获并由操作系统来处理*/

try {

if( (_access( "", 0 )) == -1 )throw 1;

}

catch(int)

{

printf( "File not existsn" );

}

}

组成,一般将被监控的代码放在try块中,然后使用if语句来判断异常是否发生,发生则throw出异常(异常可以是int型对象、字符串型对象或某个类的对象),然后由catch块捕获抛出的异常并处理。如下例:

一个未经捕获的异常是没有为其指定catch模块的异常,这样的异常将导致std::terminate()函数被调用,它又通过调用std::abort()来终止程序。

如果异常的种类很多,为了区分这些异常,可以//例二

#include

using namespace std;

int main()

{ int i;

cout<<"请输入错误号:";

cin>>i;

try

{

switch(i)

{

case 1:throw 1;break;

case 2:throw 2;break;

case 3:throw 3;break;

case 4:throw 4;break;

case 5:throw 5;break;

case 6:throw 6;break;

default:throw "不明错误";

}

}

catch(int j)

{

switch(j)

{

case 1:std::cout << "捕获" <

case 2:std::cout << "捕获" <

case 3:std::cout << "捕获" <

case 4:std::cout << "捕获" <

case 5:std::cout << "捕获" <

case 6:std::cout << "捕获" <

}

}

catch(...) {

cout<<"捕获不能识别的错误"<

}

std::cout << "程序结束。" << std::endl;

return 0;

}

为它们指定一个标号,根据发出的错误信号来分别处理这些异常。如下例:

这种方法在要处理的异常种类太大时,是不合适的。例如,如果所有的库都发出整数,catch模块将变成“拥挤且充满冲突的、各种异常处理代码堆积的地方”;为此,C++标准定义了标准异常类,而程序员们则使用标准异常类的派生类来定义和区别异常的种 类,以下是对C++中标准异常的介绍。

标准异常类

1、

语言本身或标准程序库所抛出的所有异常,都派生自基类exception。标准异常可分为三组:①语言本身支持的异常;②C++标准程序库发出的异常;③程序作用域之外发出的异常。

2、

语言本身所支持的异常

此类异常用以支撑某些语言特性,所以从某种角度来说它们不是标准程序库的一部分,而是核心语言的一部分。如果以下操作失败,就会抛出这一类异常。

 全局操作符new操作失败会抛出bad_alloc异常;

 dynamic_cast操作失败会抛出bad_cast异常;

 如果交给typeid的参数为零或空指针,将抛出bad_typeid异常;

 如果发生非预期的异常,bad_exception将会被抛出,一般情况下这将导致unexpected()函数被执行,后者通常会唤起terminate()终止程序。

3、

C++标准库发出的异常

C++标准程序库发出的异常总是派生自logic_error。

logic_error的定义:

namespace std

{

class logic_error :public exception

}

{public:

};

explicit logic_error ( const string& whatstring );

logic_error的抛出:

std::string s;

throw std::out_of_range ( s );

此外,标准程序库的I/O部分提供了一个名为ios_base::failure的特殊异常,当数据流由于错误或由于到达文件尾端而发生状态改变时,就可能抛出这个异常。

4、

程序作用域之外的异常

派生自runtime_error的异常,用来指“不在程序范围内,且不容易回避”的异常。

 range_error指出内部计算时发生区间错误

 overflow_error指出算术运算时发生上溢错误

 underflow_error指出算术运算时发生下溢错误

5、

一般情况下,标准异常总是由运行时库函数、标准/*例三 本例首先创建一个支持MFC的win32 console application,然后在工程主源文件的前面加上#include 并去除

CString strHello;

ring(IDS_HELLO);

cout << (LPCTSTR)strHello << endl;,

然后在对应位置加入下列代码*/

start: try

{

throw out_of_range("程序exception3的第37行throw out_of_range语句出现越界错误!");

}

catch(out_of_range exception3)

{

switch

(MessageBox(NULL,(),"出现错误",MB_ABORTRETRYIGNORE|MB_ICONERROR))

{

case IDABORT:std::cout<<"程序被强行终止!"<

case IDRETRY:goto start;

case IDIGNORE:;

}

}

std::cout << "程序正常结束。" << std::endl;

模板库或操作系统的API函数抛出的(并由操作系统来统一作处理的),但你也可以在自己的程序中抛出某些标准异常,这些异常只需要一个字符串参数,它将成为被what()返回的描述字符串。如下例:

这个程序首先弹出一个对话框提示出现错误,当选择[忽略]时程序输出“程序正常结束”,当选择[重试]时将不停弹出对话框,当选择[终止]时程序输出“程序被强行终止!”。(完成课堂实践内容4)

五、 学习和提高调试技巧

调试是一个程序员最基本的技能,其重要性甚至超过学习一门语言。不会调试的程序员就意味着他即int main( )

{

int high;

printf("%dn",high);

return 0;

}

使会一门语言,却不能编制出任何好的软件。要想提高自己的程序调试技巧,首先要掌握常用的调试手段,可以预见,绝大部分调试都是用最常见的调试手段所解决的,只在很少的情况下才需要更高深的调试技巧和更特殊的方法。其次要对常见错误有一定的了解,因为很多情况下都是这些常见的错误引起的程序异常,如下例是最常见的错误之一:

这个例子中展示了当没有正确地初始化变量时的结果,在程序运行之前你能否预期它的输出呢?很显然这个程序不会出现任何编译、链接和运行时错误, 但它确确实实与我们的想像不同,特别是在很长的程序中,变量的声明和变量的使用很可能相距很多行代码,甚至可能不在同一个源程序中,这就更增加了追踪此类错误的难度。在掌握了基本的调试方法之后,我们才应该去掌握那些更高深的调试技巧,如学会查看程序反汇编代码、学会查看内存、调用堆栈和寄存器值等等。

课堂实践内容:

1、

阅读MSDN中“MSDN Library - July 2001/Visual

Tool and Languages/Visual Studio 6.0 Documentation/Visual C++ Documentation/using Visual C++/Visual C++ Programmer's Guide/ Run-Time Library Reference/ Alphabetic Function Reference/ A through B/assert”中的内容并完成其中的例子程序。

2、

阅读MSDN中“MSDN Library - July 2001/Visual

Tool and Languages/Visual Studio 6.0 Documentation/Visual C++ Documentation/using Visual C++/Visual C++ Programmer's Guide/Debugging/ Debugging Techniques, Problems, and Solutions/ Using MFC Debugging Support/ Diagnostic Features/ The

TRACE Macro”中的内容并完成其中的例子程序。

3、

在上一实践内容的基础上为例子程序分别设置三类断点并启动调试器进行调试,并完成以下内容:

(1)

(2)

查看watch窗口中各变量的值及变化过程;

逐一打开[View/Debug Windows]中的子菜单,思考各有何用途;

熟悉Debug工具条的打开和各按键的作用,了解调试器中Debug菜单下各子菜单的功能。

尝试直接改变变量的值并继续开始调试,再观察运行结果。

(3)

(4)

4、

分别编译和运行本小节的三个例子程序,思考C++的异常处理机制有何好处。

课后实践内容:

5、

请认真阅读《C++标准程序库》(侯捷译 华中科技大学出版社)一书中关于标准异常的相关内容。

6、

请认真阅读本篇辅助材料《高质量C++-C编程指南》。