|
调试MFC程序
在上一讲中我们学习了视类的一般编程方法,并针对我们要使用的CListView为Schedule的视类编写了显示输出函数,大家肯定想运行一下Schedule,看看效果。但心铃估计多数朋友都无法一次性地正确输入所有代码,因此编译连接时会出现一些错误,导致工程建立失败。为了解决这些错误,我们需要学习如何排错和调试程序,在本讲中心铃就来介绍一下这方面的一些知识。 从广义的角度出发,我们把排除编译错误、连接错误和运行时调试都归为调试所应完成的工作。由于程序员在编程时会受到多方面因素的影响,程序中难免会出现各种错误,并且这些错误往往千差万别,因此调试在软件开发中占据了很重要的地位,并且调试的工作量会随着程序复杂程度的增加而呈几何级数增加。另一方面,调试工作能否顺利进行与程序员自身的经验和直觉有很大关系,经验丰富的程序员遇到问题后能够迅速发现原因所在,从而大大缩短调试的时间,而初学者在面对错误时则往往不知道从何下手。因此,心铃建议朋友们要多动手多实践,因为只有积累了足够的经验后你才会对调试感到得心应手。 l 排除编译错误 编译错误通常是由于源程序中存在着无法按照C/C++语法解释的代码而引起的。如果编译器发现有编译错误,那么它就不能为当前编译的模块生成目标代码,并中断工程的建立过程。VC6的编译器会把发现的错误与警告输出到Output窗口的Build一栏中,我们可以从中了解每条错误是在编译哪个模块(CPP文件)时发现的;发生在头文件或CPP文件中哪一行上;以及错误类型是什么。VC6开发环境对排除编译错误提供了良好的支持。如果我们在Output窗口中双击一条错误或警告,文本编辑器就会自动定位到发生错误的那一行,如果单击一条错误或警告后按下F1键,VC6会自动调出MSDN库,并显示出对该类型错误的详细解释。 VC6编译器可能报告的错误和警告多达一千多条,因此我们不可能掌握所有错误的含义和解决方法。事实上,由于不同的原因可能产生相同的错误,所以很多错误根本就没有一个标准的解决办法,关键还是要靠经验。我们应该掌握的是一些常见错误的解决方法,在遇到不熟悉的错误时能够通过查阅帮助来解决就行了。 能够引发编译错误的原因千差万别,如果我们将其归类的话,可以发现有些类别出现的频率特别高,心铃不准备介绍具体的编译错误如何解决,而是建议大家在遇到编译错误时应该主要检查哪些方面的原因: 检查是否有输入错误,漏输、错输或误输几乎必然会引起编译错误,而多数编译错误都是输入有误造成的。括号对不完整、漏掉每行语句结尾的分号是其中最常见的问题。我们在中文平台上编写程序时还应特别注意不能把C/C++的关键字或符号输成了全角符号; 检查需要的头文件是否已经包含,以及包含多个头文件时它们的先后顺序是否正确; 检查应该定义的变量、函数是否已定义; 检查参数或变量的类型转换是否正确。 由于C/C++语法要求比较严格,一个小错误可能会引起多条错误。假如我们把CScheduleApp::InitInstance()函数结束处的大括号漏掉了,那么由此引起的编译错误将多达15条。显然,这15条错误的原因都相同,我们只要解决了第一条错误,就不必再考虑后面14条了。因此,在排除编译错误时,我们可以在解决了一些简单错误之后重新进行编译,然后再来排除新发现的错误,这样可能会更节省时间。 l 排除连接错误 如果编译器顺利地为所有模块生成了目标代码,下一步就是由连接器来把所有模块连接在一起。与编译过程一样,连接过程中也会出现错误,导致无法生成最终的可执行文件。 连接错误要比编译错误的种类少一些,并且我们最常见的连接错误主要是Error LNK2001——unresolved external symbol “symbol”,即连接时找不到某个函数或外部变量。这通常是由于函数或变量名输入错误,或者它们所在的模块未被加入到工程中,或者工程设置上未添加需要的库文件等原因引起的。由于连接器在工作时不知道源代码的情况如何,它无法确定某个连接错误对应到源代码中哪一行。因此我们无法象处理编译错误那样通过双击错误信息来定位源代码,而只能利用文本编辑器的查找功能来确定源代码中可能出现问题的地方。 l 运行时调试 绝大多数程序员在编程时都会不可避免地出现各种各样的问题,前面两个步骤解决的主要是一些明显的输入和语法错误,而程序设计中存在的逻辑错误还需要通过运行时的调试来解决。运行时调试的目的是检查程序的各项功能是否符合设计要求,运算结果是否正确,在使用过程中是否会出现非法操作等异常现象,是否有良好的错误处理措施等。 图13-1:两个工具栏 相对来说,编译连接错误还是很容易排除的,运行时调试才是整个调试工作中最难的部分。好在VC6开发环境集成的调试器为我们提供了很多非常实用的调试工具,如果我们能够熟练地应用这些工具,便可大大加快调试的速度。 图13-2:设置断点的Breakpoints对话框 在使用这些工具之前,我们首先要能给程序设置断点。所谓断点是程序中的一个位置,我们希望程序运行到此处时暂停下来,以便检查某些变量的值是否正确,观察条件判断语句会运行到哪个分支,或者某个函数是否被调用等情况。设置断点最简单的方法是在文本编辑器中把光标放置在需要设置断点的代码行上,然后按下F9键,或者点取Build或Build MiniBar工具栏(见图13-1)中象手一样的按钮即可。再重复操作一次可取消断点。另外,如果我们在Workspace窗口的ClassView中选择一个函数名,然后按下F9键,也可以在该函数的入口处设置一个断点。 图13-3:Watch窗口 另外一种常用的设置断点的方法是使用Edit菜单中的“Breakpoints”命令(见图13-2)。这个对话框提供的功能相当强,在其中不仅可为源代码的某一行或者某个函数设置断点,还可以设置触发断点的条件,以及设置窗口函数在收到指定的某个消息时中断。大家不妨仔细研究一下这个对话框的使用方法,掌握它之后会很有好处的。 假定大家已经排除了所有的编译连接错误,我们在CScheduleView::OnInitialUpdate()的入口处设置一个断点,然后按F5键运行Schedule的调试版本。很快VC6的调试器就把Schedule停在了CScheduleView::OnInitialUpdate()函数的入口处,现在我们按F10键(Debug菜单或工具栏中的Step Over命令的快捷键)单步执行一行语句,然后就从View菜单或Debug工具栏中依次激活一些调试工具,看看它们分别有些什么作用。 首先是Watch窗口(见图13-3)。我们可以在这个窗口中输入变量名或表达式,当程序运行到断点处时,Watch窗口就会显示(或计算)出这些变量(或表达式)的值。对于变量来说,我们还可以在Watch窗口中直接修改它的值,这样在程序继续运行时,该变量就具有了新的值。需要注意的是,Watch窗口中只能显示全局变量或本函数内部的局部变量。Watch窗口提供了多个栏,我们可以在不同栏中输入不同函数的局部变量,从而避免在多个函数间切换时产生混淆。 图13-5:Call Stack窗口 图13-4:Variables窗口 第二个是Variables窗口(见图13-4)。它可以显示当前函数的所有局部变量和this指针的值,还可以自动显示最近被访问或改变过的变量的值。Variables窗口与Watch窗口有相似的地方,但也有很多区别,前者不能由我们自己输入变量名或表达式,但它支持显示当前处于调用堆栈中的所有函数的局部变量,只要我们从其上方的下拉列表框中选择一个函数即可。 图13-6:Memory窗口 第三个是Call Stack窗口(见图13-5)。它可以显示当前调用堆栈的情况,包括每级调用传递的参数值。双击该窗口中的某个函数,便可让文本编辑器定位于该函数内下一条要执行的语句上。另外在Call Stack窗口内选中一个函数后,还可以直接按下F9键为该函数下一条要执行的语句设置一个断点。 第四个是Memory窗口(见图13-6)。它能以16进制和ASCII码的形式显示当前进程空间的任何一个地址的值,该窗口常用来检查或修改缓冲区内的值。 第五个是Registers窗口(见图13-7)。它用来显示或修改CPU内部各个寄存器的值,注意在修改某些寄存器时应十分小心,因为不恰当的值很容易引起非法操作。 图13-7:Registers窗口 第六个是Disassembly窗口。它能把程序以汇编语言的形式显示在文本编辑器之中,熟悉汇编语言的朋友可以通过它了解一下C/C++代码与汇编代码之间的关系。 这六个调试工具中的前三个最为常用,心铃建议大家多多练习一下它们的使用方法。 在断点处观察或修改完变量的值后,我们可以按F5键继续执行程序。但更多的时候我们希望能够让程序单步执行,以便跟踪观察变量值的变化情况,或者检查程序运行的流程。为此我们可以点取Debug工具栏(见图13-1)上的跟踪执行按钮,以便单步跟踪进下一个函数(Step Into)、执行至本函数体内的下一条语句(Step Over)、跳出本函数(Step Over)、或者运行到光标处(Run to Cursor)。灵活应用这些执行方式可以大大提高跟踪调试的效率。 在调试OnDraw()这样的函数时,我们最好把VC6主窗口缩小,不让它遮盖住被调试程序的窗口。否则从OnDraw()内的断点处继续执行后立即又会重新进入该函数,导致无法看到该函数的输出效果。 Output窗口的Debug栏在调试中也很有用处。有时我们想观察某些变量的值在运行过程中的变化情况,但又不想让程序频繁中断,便可以利用TRACE、TRACE0、TRACE1、TRACE2和TRACE3等宏向Debug栏中输出信息(这些宏的含义和用法请参考MSDN库)。另外,MFC类库内部一些函数也会向Debug栏输出警告信息,这些信息可以帮助我们确定程序中哪些地方还存在问题。例如现在我们关闭Schedule,就会在Debug栏的底部发现“Detected memory leaks!”之类的信息,原因是到目前为此我们还未添加用来释放事件条目占用的内存的代码。MFC类库在程序退出时检查到程序中存在着“内存漏洞”,于是就向Output窗口输出了相应的信息。根据这些信息,下面我们就来为CScheduleDoc的构造函数和析构函数添加一些代码: CScheduleDoc::CScheduleDoc() { m_pNearestTaskUp=NULL; m_pNearestTaskDown=NULL; m_ScheduleList.RemoveAll(); } CScheduleDoc::~CScheduleDoc() { struct ScheduleItem *pSI; POSITION pos=m_ScheduleList.GetHeadPosition(); while(pos=NULL) { pSI=(struct ScheduleItem *)(m_ScheduleList.GetAt(pos)); delete pSI; m_ScheduleList.GetNext(pos); } m_ScheduleList.RemoveAll(); } 如此一来“内存漏洞”就不存在了,消除了程序中存在的一个潜在隐患。细心的朋友可能会注意到Debug栏中还有一条“CArchive exception: endOfFile.”,这是什么意思呢?需要我们解决吗?这一点就留给大家自己来判断吧。 现在我们已让Schedule具有了文件I/O和显示输出功能,但观察程序的界面就会觉得太呆板了。好吧,下一讲中我们就来学习一下资源编辑器的使用方法,画几个简单图标来为程序增添一点点色彩。
|
一共有 0 条评论