在开发程序的过程中,经常需要查找程序中的错误,这就需要利用调试工具来帮
助你进行程序的调试,当然目前有许多调试工具,而集成在VC中的调试工具以其
强大的功能,一定使你爱不释手。下面我们先来介绍VC中的调试工具的使用。
1 VC调试工具
1.1 调试环境的建立
在VC中每当建立一个工程(Project)时,VC都会自动建立两个版本:Release
版本,和Debug版本,正如其字面意思所说的,Release版本是当程序完成后,准备
发行时用来编译的版本,而Debug版本是用在开发过程中进行调试时所用的版本。
DEBUG 版本当中,包含着MICROSOFT 格式的调试信息,不进行任何代码优化
,而在RELEASE 版本对可执行程序的二进制代码进行了优化,但是其中不包含任
何的调试信息。
在新建立的工程中,你所看到是DEBUG版本,若要选择RELEASE版本,可以选择菜
单PROJECT中的SETTING命令,这时屏幕上面弹出PROJECT SETTEING 对话框,在
SETTING FOR下拉列表中选择RELEASE,按OK退出,如图4.1。
图4.1
在调试程序的时候必须使用DEBUG版本,我们可以在Project Setting 对话框的
C/C++页中设置调试选项。
图4.2
各个选项的含意如下:
· Program Database表示产生一个存储程序信息的数据文件(.PDB),它包含
了类型信息和符号化的调试信息;
· Line Numbers Only表示程序经过编译和链接产生的.OBJ或.EXE文件仅仅
包含全局和外部符号以及行号信息;
· C7 Compatible表示产生一个.OBJ或.EXE文件行号信息以及符号化的调试
信息;
· None表示不产生任何调试信息。
1.2调试的一般过程
调试,说到底就是在程序的运行过程的某一阶段观测程序的状态,而在一般情况
下程序是连续运行的,所以我们必须使程序在某一地点停下来。所以我们所做的
第一项工作就是设立断点。其次,再运行程序,当程序在设立断点处停下来时,再
利用各种工具观察程序的状态。程序在断点停下来后,有时我们需要按我们的要
求控制程序的运行,以进一步观测程序的流向,所以下面我们依次来介绍断点的
设置,如何控制程序的运行以及各种观察工具的利用。
1.3 如何设置断点
在VC中,你可以设置多种类型的断点,我们可以根据断点起作用的方式把这些断
点分为三类:1、与位置有关的断点;2、与逻辑条件有关的断点3、与WINDOWS消
息有关的断点下面我们分别介绍这三类断点。
首先我们介绍与位置有关的断点。
1、 最简单的是设置一般位置断点,你只要把光标移到你要设断点的位置,
当然这一行必须包含一条有效语句的;然后按工具条上的add/remove
breakpoint 按钮或按快捷键F9;这时你将会在屏幕上看到在这一行的左边出现
一个红色的圆点表示这二设 立了一个断点。
图4.3
2 、有的时候你可能并不需要程序每次运行到这儿都停下来,而是在满足一定条
件的情况下才停下来,这时你就需要设置一种与位置有关的逻辑断点。要设置这
种断点我们只需要从EDIT 菜单中选中breakpoint命令,这时Breakpoint对话框
将会出现在屏幕上。选中Breakpoint对话框中的LOCATION标签,使LOCATION 页
面弹出,如图4.4
图4.4
单击condition按钮,弹出Breakpoint对话框,在Expression编辑框中写出你
的逻辑表达式,如X>=3或a+b>25,最后按OK返回。
图4.5
这种断点主要是由其位置发生作用的,但也结合了逻辑条件,使之更灵活。
3、有时我们需要更深入地调试程序,我们需要进入程序的汇编代码,因此我们
需要在在汇编代码上设立断点:要设立这种断点我们只需从View菜单中选Debug
window命令 ,
图4.6
再选Disassembly子命令,这时汇编窗口将会出现在屏幕上。
图4.7
在图4.7中的汇编窗口中你将看到对应于源程序的汇编代码,其中源程序是用黑
体字显示,下面是且对应的汇编代码。要设立断点,我们只需将光标移到你想设
断点处然后点击工具条上的Insert/Remove Breakpoints 按钮,此后你将会看到
一个红圆点出现在该汇编代码的右边。
图4.8
上面所讲的断点主要是由于其位置发挥作用的,即当程序运行到设立断点的地方
时程序将会停下来。但有时我们设立只与逻辑条件有关的断点,而与位置无关。
所以下面介绍一下与逻辑条件有关的断点。
(1)逻辑条件触发断点的设置:
l 从EDIT 菜单中选中breakpoint命令,这时屏幕上将会出现Breakpoint对话
框。
图4.9
l 选中Breakpoint对话框中的DATA标签,对应的页面将会弹出
图4.10
l 在图4.10的DATA页面中的Expression编辑框中写出你的逻辑表达式,如
(X==3);
图4.11
l 最后按OK返回。
其他几种断点的设置的方法都与之类似。我们一一加以说明。
(2)监视表达式发生变化断点:
l 从EDIT 菜单中选中breakpoint命令,这时屏幕上将会出现Breakpoint对话
框。
l 选中Breakpoint对话框中的DATA标签,对应的页面将会弹出
l 在Expression编辑框中写出你需要监视的表达式
l 最后按OK键返回。
(3)监视数组发生变化的断点:
l 从EDIT 菜单中选中breakpoint命令,这时屏幕上将会 出现Breakpoint对
话框。
l 选中Breakpoint对话框中的DATA标签,对应的页面将会弹出
l 在Expression编辑框中写出你需要监视数组名;
l 在Number of Elements 编辑框输入你需要监视数组元素的个数;
l 按OK键返回。
(4)监视由指针指向的数组发生变化的断点:
l 从EDIT 菜单中选中breakpoint命令,这时在屏幕上将会出现Breakpoint对
话框。
l 选中Breakpoint对话框中的DATA标签;
l 在Expression编辑框中输入形如*pointname,其中*pointname为指针变量名
;
l 在Number of Elements 编辑框输入你需要监视数组元素的个数;
l 按OK键返回。
(5)监视外部变量发生变化的断点:
l 从EDIT 菜单中选中breakpoint命令这时屏幕上将会出现Breakpoint对话框
;
l 选中Breakpoint对话框中的DATA标签;
l 在Expression编辑框中输入变量名;
l 点击在Expression编辑框的右边的下拉键头;
l 选取Advanced选项,这时Advanced Breakpoint 对话框出现;
l 在context框中输入对应的函数名和(如果需要的话)文件名;
l 按OK键关闭Advanced Breakpoint 对话框。
l 按OK键关闭Breakpoints 对话框。
(6)在讲了位置断点和逻辑断点之后我们再讲一下与WINDOWS消息有关的断点。
注意:此类断点只能工作在x86 或 Pentium 系统上。
l 从EDIT 菜单中选中breakpoint命令,这时屏幕上将会出现Breakpoint对话
框;
l 选中Breakpoint对话框中的MESSAGE标签,对应的页面将会弹出;
l 在Break At WndProc 编辑框中输入Windows 函数的名称;
l 在Set One Breakpoint From Each Message To Watch 下拉列表框中选择
对应的消息;
l 按OK 返回。
1.4 控制程序的运行
上面我们讲了如何设置各类断点,下面我们来介绍如何控制程序的运行。当我们
从菜单Build到子菜单Start Debuging 选择Go 程序开始运行在Debug状态下,程
序会由于断点而停顿下来后,可以看到有一个小箭头,它指向即将执行的代码。
图4.12
随后,我们就可以按要求来控制程序的运行:其中有四条命令:Step over,
step Into , Step Out ,Run to Cursor。
图4.13
在图4.13中:
Step over 的功能是运行当前箭头指向的代码(只运行一条代码)。
Step Into的功能是如果当前箭头所指的代码是一个函数的调用,则用Step Into
进入该函数进行单步执行。
Step Out的功能是如当前箭头所指向的代码是在某一函数内,用它使程序运行至
函数返回处。
Run to Cursor的功能是使程序运行至光标所指的代码处。
1.5 查看工具的使用
调试过程中最重要的是要观察程序在运行过程中的状态,这样我们才能找出
程序的错误之处。这里所说的状态包括各变量的值,寄存中的值,内存中的值,
堆栈中的值 ,为此我们需要利用各种工具来帮助我们察看程序的状态。
¨ 弹出式调试信息泡泡(Data Tips Pop_up Information)。
当程序在断点停下来后,要观察一个变量或表达式的值的最容易的方法是利用调
试信息泡泡。要看一个变量的值,只需在源程序窗口中,将鼠标放到该变量上,
你将会看到一个信息泡泡弹出,其中显示出该变量的值。
图4.14
要查看一个表达式的值,先选中该表达式,仍后将鼠标放到选中的表达式上,同
样会看到一个信息泡泡弹出以显示该表达式的值如图4.15所示。
图4.15
¨ 变量窗口(VARIABLE WINDOW)。
在VIEW 菜单,Debug window选 Variables window; 变量窗口将出现在屏幕上。
其中显示着变量名及其对应的值。你将会看到在变量观察窗口的下部有三个标签
:AUTO ,LOCAL,THIS 选中不同的标签,不同类型的变量将会显示在该窗口中。
图4.16
¨ 观察窗口(WATCH WINDOW):
在VIEW 菜单,选择Debug window 命令,Watch window 子命令。这时变量窗口
将出现在屏幕上。
图4.17
在图4.17的观察窗口中双击Name栏的某一空行,输入你要查看的变量名或表达式
。
图4.18
回车后你将会看到对应的值。观察窗口可有多页,分别对应于标签
Watch1,Watch2,Watch3等等。假如你输入的表达式是一个结构或是一个对象,你
可以用鼠标点取表达式右边的形如 + ,以进一步观察其中的成员变量的值如图
4.19。
图4.19
¨ 快速查看变量对话框(quick watch);
在快速查看变量对话框中你可以象利用观察窗口一样来查看变量或表达式的值。
但我们还可以利用它来该变运行过程中的变量,具体操作如下:
(1) 在Debug 菜单,选择Quick Watch命令,这时屏幕上将会出现Quick
Watch 对话框;
图4.20
(2) 在Expression 编辑框中输入变量名,按回车;
图4.21
(3)在Current Value 格子中将出现变量名及其当前对应的值如图4.22:
图4.22
(4)如要改变该变量的值只需双击该变量对应的Name 栏,输入你要改变的
值;
(5)如要把该变量加入到观察窗口中,点击Add watch 按钮;
(6)点击Close 按钮返回;
¨ 我们还可以直接查看内存中的值
(1)从View菜单中选取Debug windows 及Memory 子命令。Memory Window 出现
;
图4.23
(2)在Address 编辑框中输入你要查看的内存地址,回车。对应内存地址中的
值将显示在Memory window 的窗口中。
图4.24
¨ 在调试过程中,有时我们需要查看或改寄存器中的值。我们只需:
(1)从View 菜单中选取Debug window 及 Registers 子选项。Registers
窗口出现。在Registers 窗口中,信息以 Register = Value 的形式显示,其中
Register 代表寄存器的名字,Value 代表寄存器中的值。
图4.25
(2)如果你要修改某一个寄存器的值,用TAB键,或鼠标将光标移到你想改
变的值的右边,然后输入你想要的值。回车返回。
在寄存器中,有一类特殊的寄存器称为标志寄存器,其中有八个标志位:
OV是溢出标志;
UP是方向标志;
EI是中断使能标志;
Sign 是符号标志,
Zero是零标志。
Parity是奇偶较验标志。
Carry 是进位标志。
2 高级调试技术
前面我们讲了调试工具的使用,利用它可以就进行常规的调试,即使程序在某处
停下来,再观察程序的当前壮态。而且这些工具在且它调试器中也有。但我们知
道我们知道在VC程序的开发过程中,光有这些工具是不够的。为了更快更好地开
发程序,我们还需要利用更高级的调试工具。我们知道,在利用VC开发过程中,
利用MFC将会极大地方便应用程序的开发,所以开发人员往往是利用MFC来开发应
用程序,正是这个原因Microsoft公司在MFC中提供了一些特性来帮助你进行程序
的调试。
我们知道在MFC中,绝大多数类都是从一个叫做Cobject的类继承过来的,虽然这
是一个虚基类,但它定义了许多成员函数,其中许多成员函数是用来支持程序的
调试的,如Dump ,Assertvalid 等成员函数。另外他们都支持如TRACE,ASSERT等
宏,并支持内存漏洞的检查等等。我们知道,为了支持调试,类库肯定在在性能
上有所损失,为此Microsoft 公司提供了两个不同的版本的类库:Win32 Debug
版本和Win32 Release版本。在前面我们已经提到,每当我们建立一个工程时,
我们也有对应的两个版本。在你的DEBUG 版本的工程中,编译器连接DEBUG 版本
的MFC类库;在你的RELEASE 版本的工程中编译器连接RELEASE版本的MFC 类库以
获得尽可能快的速度。下面我们来介绍这些工具的利用。
2.1 TRACE 宏的利用
TRACE 宏有点象我们以前在C语言中用的Printf函数,使程序在运行过程中
输出一些调试信息,使我们能了解程序的一些状态。但有一点不同的是:TRACE
宏只有在调试状态下才有所输出,而以前用的Printf 函数在任何情况下都有输
出。和Printf 函数一样,TRACE函数可以接受多个参数如:
int x = 1;
int y = 16;
float z = 32.0;
TRACE( "This is a TRACE statement/n" );
TRACE( "The value of x is %d/n", x );
TRACE( "x = %d and y = %d/n", x, y );
TRACE( "x = %d and y = %x and z = %f/n", x, y, z );
要注意的是TRACE宏只对Debug 版本的工程产生作用,在Release 版本的工程中
,TRACE宏将被忽略。
2.2 ASSERT宏的利用
在开发过程中我们可以假设只要程序运行正确,某一条件肯定成立。如不成立
,那么我们可以断言程序肯定出错。在这种情况下我们可以利用ASSERT来设定断
言。ASSERT宏的参数是一个逻辑表达式,在程序运行过程中,若该逻辑表达式为
真,则不会发生任何动作,若此表达式为假,系统将弹出一个对话框警告你,并
停止程序的执行。同时要求你作出选择:Abort,Ignore,Retry。若你选择
Abort,系统将停止程序的执行;若你选择Ignore 系统将忽略该错误,并继续执
行程序;若你选择Retry ,系统将重新计算该表达式,并激活调试器。同TRACE
宏一样,ASSERT宏只DEBUG版本中起作用,在RELEASE版本中ASSERT宏将被忽略。
2.3 ASSERT_VALID宏的利用以及类的AssertValid()成员函的重载
ASSERT_VALID宏用来在运行时检查一个对象的内部合法性,比如说现在 有
一个学生对象,我们知道每个学生的年龄一定大于零,若年龄小于零,则该学生
对象肯定有问题。事实上,ASSERT_VALID宏就是转化为对象的成员函数
AssertValid()的调用,只是这种方法更安全。它的参数是一个对象指针,通过
这个指针来调用它的AssertValid()成员函数。
与此相配套,每当我们创建从Cobject类继承而来的一个新的类时,我们可以重
载该成员函数,以执行特定的合法性检查。
2.4对象的DUMP函数的利用
Dump 函数用来按指定的格式输出一个对象的成员变量,来帮助你诊断一个
对象的内部情况。与AssertValid成员函数一样,Dump也是Cobject 类的成员函
数。Dump函数的参数是一个CdumpContext对象,你可以象利用流一样往向这个对
象中输入数据。当你创建一个Cobject继承而来的 新类时,你可以按如下步骤重
载你自己的Dump函数:
(1) 调用基类的Dump函数,以输出基类的内容;
(2) 向Cdumpcontest对象输出该类的数据.
例如,典型的Dump函数定义如下:
#ifdef _DEBUG
void CPerson::Dump( CDumpContext& dc ) const
{
// call base class function first
CObject::Dump( dc );
// now do the stuff for our specific class
dc << "last name: " << m_lastName << "/n"
<< "first name: " << m_firstName << "/n";
}
#endif
你可能已经注意到整个函数的定义都包含在#ifdef _DEBUG 和#endif中,这
使得Dump成员函数只在DEBUG版本中发生作用,而对RELEASE版本不发生作用。
3 内存漏洞的检查
也许你已经知道,在C++和C语言中指针问题也就是内存申请与释放是一个令人头
疼的事情,假如你申请了内存,但没有释放,并且你的程序需要长时间地运行,
那么,系统的资源将逐渐减少,当系统的资源全部被用完时,系统将会崩溃。所
以在开发程序的过程中一定要保证资源的完全释放。下面我们来介绍内存漏洞的
检查。
也许你会问,系统是怎样支持内存漏洞的检查的?其实在你的Debug版本中所有
的有关内存分配的函数都是被重载过的,具体过程是这样的,当你的程序申请内
存时,它首先调用一般的内存分配函数分配一块稍大的内存块。在这一内存块中
分为四个小块:Heap Information, buffer , User memory block, buffer。第
一块为有关堆的信息,比如,申请该内存的地点(文件名,行号),此内存块的类
型(如整型,浮点,或某一类的对象)等等。第二块是一个缓冲区,用于截获用户
对其申请内存使用越界的情况。第三块是真正给用户的内存,返回的指针也是指
向这儿。第四块也是一个缓冲区,作用同第二块。
当你申请的内存均被记录在案后,要检查内存漏洞就比较容易了,粗略地说,假
如你要检查某一程序段是否有内存漏洞,你只需在这一程序 段的开始要求系统
为你做一个内存使用情况的映象,记录下程序开始时的内存使用情况,然后在程
序段的末尾再使系统为你做一次内存映象,比较两次映象,以检查是否有没释放
的内存,假如有未释放的内存,根据这一块中有关分配情况的信息来告诉用户在
那儿申请的内存没释放。
具体地讲检查内存漏洞需要以下几个步骤:
l 在你所检测的程序段的开始处建立一个CmemoryState对象,调用其成员函
数Checkpoint,以取得当前内存使用情况的快照;
l 在你所检测的程序段的末尾处再建立一个CmemoryState 对象,调用其成员
函数Checkpoint ,以取得当前内存使用情况的快照;
l 再建立第三个CmemoryState 对象,调用其成员函数Difference,把第一个
CmemoryState对象和第二个CmemeoryState对象作为其参数.,如果两次内存快照
不相同,则该函数返回非零,说明此程序 段中有内存漏洞。下面我们来看一个
典型的例子:
// Declare the variables needed
#ifdef _DEBUG
CMemoryState oldMemState, newMemState, diffMemState;
OldMemState.Checkpoint();
#endif
// do your memory allocations and deallocations...
CString s = "This is a frame variable";
// the next object is a heap object
CPerson* p = new CPerson( "Smith", "Alan", "581_0215" );
#ifdef _DEBUG
newMemState.Checkpoint();
if( diffMemState.Difference( oldMemState, newMemState ) )
{
TRACE( "Memory leaked!/n" );
}
#endif