VC 调试版(Debug Version)和发行版(Release Version)
调试是纠正或修改代码,使之可以顺利地编译、运行的过程。为此,VC IDE提供了功能强大的调试和跟踪工具。
1.1.1 调试版(Debug Version)和发行版(Release Version)
开发环境总是为你的工程创建调试版和发行版。在调试版里,我们排查各种可能的程序错误,然后制作成发行版以获得较好的信息。这就是调试版与发行版的区别:前者包含了较多调试信息,最终执行文件较大,性能较差;后者最终执行文件较小,性能更好。具体地讲,发行版和调试版区别有:
1. 调试版下,可以使用诊断和跟踪宏,如ASSERT和TRACE,MFC类的DUMP功能也定义在调试版下。
2. 额外的变量初始化。编译器会自动将你未初始化的变量逐每字节赋值为0xCC。
3. 内存分配监视。在调试版下,在堆上分配的内存都会记录,做额外的初始化(0xCD),在释放时,会将其内容逐字节置0xFD。
怎样配置调试版[C1] 或发行版?
调试版和发行版都可以从 Build | Configurations中增加,或者当这两个配置已存在的情况下,从Project | Settings命令下进行配置。两者在配置上主要有以下不同:
1. 两个版本应该配置成不同的输出文件夹。这在Project | Settings | General中的两个编辑框中设定。
2. 调试版下,程序应该链接Debug版的c运行时库、禁止代码优化,并在C++属性页中的Preprocessor类别中加上预定义标识符_DEBUG。在Link属性页中的Debug类别中,选中DebugInfo和Microsoft Format选框。
4. 发行版下,程序应该链接Release版的c运行时库、设置代码优化,不定义_DEBUG标识符,去掉DebugInfo选框中的选择。
1.1.2 排除编译错误
VC6.0的编译器可以报告大约1100个左右的错误,这还不包含警告。而在编译和调试过程中我们还不仅仅遇到编译错误,也可能遇到链接错误和其他错误,因此我们只能讲一些很常见、很重要的编译错误。对我们没有列出来的编译错误,解决它的最好办法是,在output窗口中将光标定位在输出行,然后按F1键以获得MSDN的帮助。如果MSDN的版本与你的VC不兼容,那么你可以用获得到Error xxxx作为关键字在MSDN中搜索。目前这些错误的说明帮助出现在Visual C++ Programmer's Guide中的Build Errors条目下。
并非错误
形如“Loaded 'C:\WINNT\system32\gdi32.dll', no matching symbolic information found.”这样的报告不是错误。一般情况下,我们只需要跟踪自己的源代码,检查在那里发生了什么错误。但有时候我们也想看看,如果一个错误报告在系统DLL里时究竟是怎么回事。Microsoft不愿意在系统DLL中包含调试信息,因此它提供调试符号帮助你跟踪。随VC6.0发行的有一个调试符号包,但其内容可能不能跟得上你操作系统版本。如果你需要适合你操作系统版本的调试符号,请到微软更新网站上下载。
l LNK2001错误
LNK2001错误报告一个不能识别的外部标识符。通常你使用一个函数或外部变量,都必须事先给出其声明,否则就会产生这样一个错误。可以从以下几个方面寻找原因:
1. 使用内联函数,但该内联函数定义在.cpp文件中,而不是在.h文件中。
2. 声明了函数或外部变量,但找不到它们的定义。
3. 程序入口设置错误,并且进一步提示_WinMain@16': Unresolved External Symbol或类似错误。在Windows程序中,程序并不是最先从WinMain()或Main()这样的函数处开始执行,而是从WinMainCRTStartup()或MainCRTStratup函数处开始执行。这四个函数(实际上是8个,每个函数都有UNICODE版和单字节版)必须配套。如果进入点是WinMainCRTStartup (),则它期待在你的程序中找到一个WinMain()函数,如果找不到,则发生上述错误。
4. 在程序中使用了线程启动函数_beginthread,但选择的链接库为LIBC.LIB。
其他错误见MSDN。
1.1.3 断点和跟踪
编译错误都是静态的,更多的错误发生在运行时。为了排除运行时的错误,首先需要的是能够使正在运行的代码停下来。VC6.0提供了多种断点设置。
l 位置断点
l 条件断点:可以设置以下条件为真时中断程序
1. 单变量值改变时
2. 单变量值判断条件中断,如K>0,i == 5等
3. 数组单元素或多元素值改变或值判断条件中断。
4. 指针:指针变量改变或指针所指内容改变
5. 监视固定内存值变化
6. 监视寄存器中值的变化,如 cs == 0等。
也可以直接输入断点设置表达式,其语法是:
①Advanced Syntax 详解
{[function],[source],[exe] } location
{[function],[source],[exe] } variable_name
{[function],[source],[exe] } expression
括在{}对中的是上下文设置,这三项中任意一项都可省略,但省略前面的项时,应注意保留逗号。如下面的设置
是错误的:
{File.c, File.exe} .143 - Bad
正确的设置如
{Fun} .143 这样在函数Fun的第143行设置了一个断点。
更详细的说明见下面的举例:
{[function],[source],[exe] } .100 - A line number (this may not work with some languages)
{[function],[source],[exe] } @100 - A line number (this works for all languages)
{[function],[source],[exe] } Traverse - A function name
{[function],[source],[exe] } CMyWindow::OnCall - A function name
{[function],[source],[exe] } 00406030 - A memory address (decimal)
{[function],[source],[exe] } 0×1002A - A memory address (hexadecimal)
{function,[source],[exe] } Label - A statement label. The context must include function since
Label is visible in the function’s scope.
更详细的帮助,查看Visual C++ Programer’s Guid中Setting Breakpoints When Values Change or Become True主题。
l 消息断点
l 异常断点
当程序发生异常时,我们应该立即停下来,看看发生了什么事情。默认地,当一个异常发生时,Debugger 将异常信息写到Output 窗口中,并根据你的选择决定是否立即中断程序。这个设置在Debug菜单中的Exceptions选项中提供。这里提供了两个选项Stop Always和Stop If Not Handled。当Stop If Not Handled被选中时,debugger仅仅往Output窗口中输出一条信息,而并不中止程序,除非没有这个异常最后也得不到处理。在有些情况下,这会是一个有些晚的时机。当Stop Always被选中时,debugger会在异常处理代码接管以前就中断程序。
1.1.4 查看和修改程序运行状态
开发环境提供了一系列的查看方式来临视甚至修改程序的运行状态。总共提供了六种查看窗口:
l Watch窗口
Watch窗口用来查看和解码中间变量,并允许你修改变量的值。Watch窗口共有四个Tab。每个Tab都是一个ListView,由两栏组成。左边一栏为变量名,右边一栏则为变量取值。只要在变量名栏输入变量名并回车,就可以得到当前变量的值。下面是一些特殊的用法:
1. 列出数组中前n个元素的值:arr,n
2. 查看UNICODE编码的字符串值:str,su
3. 已知消息值,查对应的宏:messageValue,wm
4. 查看窗口类标志(Window class flag):flagValue,wc
5. 查看当前线程最后错误报告:@err,hr
6. 查看COM库最后错误报告:@hres,hr
7. 查看当前寄存器值,把寄存器名当作变量名输入就可以了。
直接在右边栏输入新的取值,就会修改该变量的取值并发生作用。
l Variables窗口
这个窗口始终跟踪当前上下文中重要的变量取值。但不能增加对变量的跟踪。它包含三个Tab:
Auto窗口跟踪当前声明或前一个声明中使用的变量,当你从一个函数调用中跳出时,还可以显示函数的返回值。
Locals窗口显示对当前函数来说是局部的变量的取值。
This窗口显示this指针所指对象取值。
此外,在Variables窗口的上部,还有一个Context的组合框,可以用来切换当前查看的上下文。
l CallStack窗口
这个窗口显示了函数调用的情况。当你的程序出现一个断言时,你可以用这个窗口把函数调用顺序调出来,并且可以知道调用时传递参数的情况。双击窗口中列出的函数,可以定位到源代码中。
l Memory窗口
Memory窗口的上部是一个地址输入框。你可以在这里输入0×00400000,因为这时进程的起始点,所以毫不奇怪,你会看到MZ这两个字。Memory窗口支持定制显示格式,并支持表达式。
在Memory窗口的下面窗口处按右键,会出现一个菜单,允许你按单字节、双字节十六进制或四字节十六进制来显示内存的内容。
如果你愿意麻烦一点,还有办法得到更准确的显示。点Tools | Options,选择Debug属性页,在这里可以决定以什么形式显示内存内容以及从什么地址开始显示。这里提供了14种格式选项。此外,你还可以输入一个地址表达式。比如,如果要知道当前栈顶的情况,在这里输入ESP。
l Disassembly窗口
这个窗口可以用跟踪优化代码,或者是复杂表达式。
l Registers窗口
Registers窗口显示所有寄存器值,并允许你修改。它与Watch窗口的区别是,你不需要记住寄存器名,它总是把所有寄存器值给你列出来。
1.1.5 使用断言和诊断[1]
VC提供了两个断言宏:ASSERT和VERIFY。两者的区别是,在发行版中,ASSERT行不会执行,因为在发行版中,ASSERT宏被定义成
#define ASSERT /##/
这样这行代码被注释掉,从而不会被编译。VERIFY宏在发行版中被定义成
#define VERIFY
这样VERIFY后面的代码仍然加入编译,从而得到执行。
在你掌握了这两个断言的区别后,我们将只讲述ASSERT宏。ASSERT是一个带参宏,其参数允许是一个表达式。当表达式的值为假时,程序发生中断并提示你是否进行调试。
除了使用条件判断表达式外,ASSERT(0)也很有用。它始终引起一个断言错,并提示你是否调试。你可以把它放在一些你不期望程序运行到达的路径上,如果事情一旦发生,则可以直接进行调试。
VC还提供TRACE宏,以及它的姊妹宏TRACE0,TRACE1,TRACE2,TRACE3。TRACE宏的语法类似于printf,包括格式字符串的规定也是一致的。后面三个宏限定你使用一个格式字符串和一至三个变量。TRACE输出到Output窗口,如果工具MFC TRACER中设置为允许的话。
MFC还提供了对象诊断工具,即名为Dump的函数。MFC库中每个从CObject继承的类都有Dump的能力。如果你从MFC类库中派生了某个类,在有必要的情况下,最好重载这个函数。要记住,你重载的函数应该调用基类的Dump函数。另外,Dump函数只在调试版下可用。因此重载的函数也应该括在#ifdef _DEBUG宏中。