在VC程序中使用调试语句

转载 2012年03月27日 08:58:01

VC程序中使用调试语句

  为了更好地对程序调试,可以使用如下方法:使用断言、使用跟踪语句、使用异常和返回值。

一、断言

1、基本概念

  断言是一种让错误在运行时候自我暴露的简单有效实用的技术。它们帮助你较早较轻易地发现错误,使得整个调试过程效率更高。

  断言是布尔调试语句,用来检测在程序正常运行的时候某一个条件的值是否总为真,它能让错误在运行时刻暴露在程序员面前。使用断言的最大好处在于,能在更解决错误的发源地的地方发现错误。断言具有以下特征:

.断言是用来发现运行时刻错误的,发现的错误是关于程序实现方面的。

.断言中的布尔表达式显示的是某个对象或者状态的有效性而不是正确性。

.断言在条件编译后只存在于调试版本中,而不是发布版本里。

.断言不能包含程序代码。

.断言是为了给程序员而不是用户提供信息。

  使用断言最根本的好处是自动发现许多运行时产生的错误,但断言不能发现所有错误。断言检查的是程序的有效性而不是正确性,可通过断言把错误限制在一个有限的范围内。当断言为假,激活调试器显示出错代码时,可用Call Stack命令,通过检查栈里的调用上下文、少量相关参数的值以及输出窗口中Debug表的内容,通常能检查出导致断言失败的原因。_ASSERTE(属于C运行时间库)还能在断言失败时显示出失效断言。下面我们讨论一下MFC库中的断言。

2MFC库中的断言

(1) ASSERT(布尔表达式)

MFC时最好选择ASSERT宏,它的优点是即使出现了WM_QUIT消息也能显示断言失效消息框。

(2) VERIFY(布尔表达式)

VERIFY宏中的布尔表达式在发布版本中被保留下来。VERIFY宏简化了对函数返回值的检查,一般用来检查Windows API的返回值。由于VERIFY宏里的布尔表达式在发布版本里保留了下来,因此最好尽量不要使用这个宏以实现程序代码和调试代码的完全分离。

(3 )ASSERT_VALID(指向CObject派生类对象的指针)

ASSERT_VALID宏通过调用重载的AssertValid函数来确定指向CObject派生类对象的指针是否有效。无论你什么时候从CObject派生类中得到一个对象,在对这个对象做任何操作之前都应该调用ASSERT_VALID宏。

(4) ASSERT_KINDOF(类名指向CObject派生类对象的指针)

这个宏用来验证指向CObject派生类对象的指针是否从某个特殊类中派生,在调用它之前先调用ASSERT_VALID宏。只有在很特殊的场合下才用得到,如检测编译器可能错过的对象类型问题。

此外,还有两个没有正式文件的ASSERT宏的变种:ASSERT_POINTER(指针,指针类型)ASSERT_NULL_OR_POINTER(指针,指针类型)

3、什么时候使用断言

  把断言看作一种简单的制造栅栏的方法,这种栅栏能使错误在穿过自己时暴露。

.检查函数的输入

.检查函数的输出

.检查对象的当前状态

.坚持逻辑变量的合理性和一致性

.检查类中的不变量

公有成员函数比私有和保护的成员函数需要更全面的断言。

  不正确地使用断言会导致错误。断言应该检测那些在程序正常运行的时候永远都不可能出现的状态。断言是用来揭示错误的,而不是用来纠正运行时刻错误的。

4、断言与防御性编程(Defensive Programming)

  断言在调试的时候向程序员揭示运行时刻错误(调试版本里),而防御性编程使用户在运行程序(发布版本里)时,当出现意外情况时程序仍能继续工作。实际上,防御性的编程要求程序在检测到意外时返回一个安全的值(比如布尔函数返回false,指针和句柄返回空值),一个错误代码或者抛出一个异常来解决问题。特定的防御性编程技术包括:处理无效函数参数和数据、出现问题的时候程序失败、检查临界函数返回的错误代码以及处理异常。需要防御性编程的标准问题包括:错误的输入数据、内存或者硬盘空间不够、不能打开一个文件、外部设备不能访问、网络连接不上或者甚至在程序中还有错误,目的是保持程序的运行状态。如果你的程序是防御性的,别忘了使用断言。如果你使用断言,也别忘了防御性编程。这两种技术最好结合在一起使用。

二、跟踪语句

1、基本概念

  跟踪语句(trace statements)可使程序执行,并使程序员可对可变值进行查看。它们提供了一个用于观察的程序,并且独立于一个交互式的调试器,但是最具有特色的是它们常用于对调试器提供的信息进行补充。在VC中,跟踪消息通常输出到输出窗口中的Debug标签,也可以重新输出到一个文件中。跟踪语句的特性如下:

.跟踪语句用于报告代码中重要的运行事件。

.跟踪语句的编译通常是有条件的,并只存在于调试版本中,而在发布版本中不被编译。

.跟踪语句不能包含程序代码或对程序代码有间接的影响作用。

.跟踪语句的目的是向程序员提供信息,而不是向用户。

跟踪语句也是调试语句,它可以执行程序,并且在运行中程序员可以查看变量。跟踪语句对于那些使用交互式调试器很难调试的程序是很有效的。

跟踪语句和断言的区别如下:

.跟踪语句是无条件的,断言是有条件的布尔语句。

.跟踪语句用于显示程序执行和变量值,不直接显示bug,断言用于显示出bug

.跟踪语句将信息输出到调试窗口或文件中,可被随意地忽略,断言打断程序的执行。

2MFC中的跟踪语句

  在MFC中,你可以使用TRACEAfxOutputDebugString宏、CObject::Dump虚拟函数和AfxDumpStack函数。TRACE宏由AfxDump实现,AfxDumpAfxOutputDebugString实现。AfxOutputDebugString宏和AfxDumpStack函数可以在所有版本中编译,其他只能在调试版本中编译。

1TRACE宏有以下形式:

_TRACE(reportType,format);

_TRACE0(reportType,format,arg1);

_TRACE1(reportType,format,arg1,arg2);

_TRACE2(reportType,format,arg1,arg2,arg3);

_TRACE3(reportType,format,arg1,arg2,arg3,arg4);

MFC中,推荐使用TRACEn宏,当使用TRACE宏时需要使用_T宏来格式化参数以正确解决Unicode的校正,而TRACEn不需要。

MFC TRACE宏中的一个缺点是AfxTrace函数使用一个512字符固定大小的缓冲区,这使得它在跟踪长字符串时是无用的。

2CObject::Dump

CObject类有一个转储(dump)虚拟函数,所有继承CObject的类都可以通过重载这个函数,输出它们的值。

3Visual C++消息Pragma

消息Pragma实际上是一个编译时的跟踪语句,你可以使用它来警告在预处理过程中发现的潜在的编连(build)问题。典型的例子:

#if (WINVER>=0x0500)

#pragma message (“NOTE:WINVER has been defined as 0x0500 or greater.”)

#endif

  消息Pragma是非常有用的,尤其是在复杂编连中。然而,如果你要检测一种特定的问题,而不是潜在的问题,使用#error预处理来代替打断编译会更直接一些。

  每当你的程序中有错误而你想得到更多信息的时候,你应该去查看一下跟踪消息。由于VC输出窗口的缓冲区是有大小限制的,因此如果跟踪消息数据产生的速度超过输出窗口处理的速度,那么消息会塞满缓冲区,导致数据丢失。避免这个问题的简单方法是在输出大量数据的代码段如转储对象时,调用Sleep API函数。

三、异常

1、基本概念

  错误是一种条件,在这种条件下,如果不执行额外的处理,线程就不能正常地执行下去。异常是用于处理错误的。使用异常的一个很明显的好处就是它们通过发出错误信号,可以让程序代码和错误处理代码分开,而且不会让程序忽略错误,你不用不断地检查函数的返回值,因此它们将程序代码简单化。另一个好处是它们不需要严格的编程作风。

异常的基本特性:

.异常是基于每个进程而提出并处理的。

.异常不能被线程忽略,必须被处理。

.未处理的异常会使进程结束,而不仅仅是结束线程。

.异常出来在释放栈时会释放所有的栈对象,避免了资源的漏洞。

.异常处理需要大量的额外操作,使得它不适于经常运行的代码。

.可以抛出任何类型的异常对象,除了整数。

如果正确执行,异常处理有下面的特性:

.异常是不是正常的运行结果,是特殊情况。

.异常在返回值无效的情况下使用。

.异常是可靠的,不可能被忽略。

.异常简化了错误处理,简化了程序代码,使错误处理更加方便。

  Visual C++的默认情况下,在调试版本中处理异常,而在发布版本中并不进行处理。由于异常也是错误,Windows异常码采用了同Windows错误码一样的位映射模式,为一个32位的值,这些码由Microsoft定义,任何异常码的最高四位总是1100(二进制),即十六进制里的0xC

2Windows结构异常和C++异常

  Windows结构异常作为硬件异常(如访问非法或被零除)或操作系统异常的结果被抛出,C++异常只能由throw语句抛出。Windows结构异常处理不能处理对象的解析,因此你应该在C++程序中一直使用C++异常。然而,C++异常不能处理硬件和操作系统异常,你的程序需要将结构异常转化为C++异常。C++异常并不直接从你的程序代码中抛出而是从C++运行库中抛出,因此你需要调用栈窗口来返回你的代码。为了正确处理硬件和操作系统异常,你可以创建自己的异常类并使用_set_se_translator函数安装一个结构异常向C++异常的转化器,但不要捕获那些不能恢复所产生问题的转化后的结构异常。

3MFC中的异常

  在MFC中,所有的异常对象都是从CException基类(它有使用起来非常方便的GetErrorMessageReportError成员函数)中派生来的。大多数的MFC异常对象都是动态分配的,而且当它们被捕获时,必须被删除,而没有被捕获的MFC异常由MFC本身在AfxCallWndProc函数中捕获并删除。

4、异常的开销

  当抛出C++异常时,函数调用链将从此回溯搜索,寻找可以处理抛出这类异常的处理器。若没找到,进程结束。如果找到,调用栈将被释放,所有的自动(局部)变量也将释放,然后栈将被整理为异常处理器的上下文相关设备。因此异常开销由一个异常处理器目录和一个活动的自动变量表(它需要额外的代码、内存,而且不论异常是否抛出,都会运行),还得加上函数调用链的搜索、自动变量的解析和栈的调整(它只在抛出异常的时候需要执行)组成。

5、异常策略

(1)抛出时机

  抛出异常的时机应该是一个函数发现一个错误,如果没有一些特殊的操作,该错误能阻止程序正常的运行,而这种操作它自己不能完成,或是在函数不可能有返回值的时候。

  使用异常处理更简单,更可靠,更有效,可以创建更健壮的代码。然而,应该只在意外的情况下使用异常处理。如果你认为一个指针应该是空值,这种条件下就直接在代码中检查这个值,而不要使用异常。

(2)何时捕获

对于这个问题,有一些可能的标准:

.当函数知道如何处理这个异常时。

.当这个函数可以合理地处理这个异常而高级的函数不知道如何处理时。

.当抛出异常可能使进程崩溃时。

.当函数可以继续执行它的任务时。

.当需要整理分配好的资源时。

  异常处理的一个缺点是它可能导致资源的泄露。因此,防止资源泄露更应该是保持程序异常安全的一部分。栈释放时会自动整理局部变量,但不包括动态分配的变量。可以使用智能(smart)指针来保护你的代码在存在异常的情况下不会产生资源泄漏。

(3)怎样捕获

.MFCC++异常应该通过引用来捕获。使用引用捕获异常不需要删除异常对象(因为使用引用捕获的异常会在栈中传送),而且它保留了多态性(因此你捕获的异常对象正是你抛出的异常对象)。

.MFC异常应该通过指针来捕获。使用指针捕获异常需要你删除对象。因为它们通常从堆中分配,当你处理完异常之后,需要调用Delete成员函数来删除。你不可以使用省略捕获处理器捕获MFC异常,这会导致一个内存泄露。必须使用Delete成员函数删除MFC异常,而不用delete,因为一些MFC异常为静态对象创建。

在释放栈的过程中抛出异常会导致进程的终止。释放栈涉及到调用析构函数,异常可以阻止调用delete操作符,这样会有资源泄漏,因此异常最好不要从析构函数中抛出。如果非要在析构函数里抛出异常,必须妥善处理,避免资源泄漏。

6、异常与防御性编程

  在异常发生时继续执行程序,远比执行一个正常的关闭动作要重要。如果可能,应该将精力集中在继续执行程序,并在必须的情况下才正常地关闭程序。可能最根本的正常关闭是一个在崩溃时可以重新启动自己的进程,这是Windows资源管理器使用的一种技术。

  如果一个与错误相关的C++异常是可预料的,如果它发生在非关键性的代码中,如果它不是发生在程序启动或结束过程中或一个不可恢复的结构异常的结果中,这个程序就可以从其中恢复。

  一旦你的程序可以从与错误相关的异常中恢复,应该先检查程序的状态和它的文档。如果程序和文档已经被破坏了,进程也应该终止运行。否则,程序需要通知客户机确定动作的过程。如果客户机同意执行下去,程序应该恢复错误并继续执行。

四、返回值

  并不是在所以场合下都能使用异常,如在使用Windows API编程或带有COM编程时并不使用异常。在异常不适合的时候,使用返回值是一个好的办法。

返回值的基本特性:

.返回值可以指示正常和不正常的函数运行,但不能阻止线程的继续运行。

.返回值很容易被忽略。

.返回值在典型情况下是一个整数,通常映射符合于一个预定义的值。

.返回值能高效地传递和接收。

因此,返回值最适合用于以下的情形:

.用于非错误的状态信息

.用于大多数情况下可以随意忽略而不会出问题的错误。

.用于更易于出现在循环中的错误。

.用于中间语言模块如COM组件中的错误。

 

使用Visual C++调试器调试

一、调试版本与发布版本

  有时程序能在调试版本运行但不能运行于发布版本,反之也有可能。一般说来,一个发布版本意味着某些类型的优化,而一个调试版本则没有优化。下面我们来看看它们的区别:

1、特别针对调试版本的编译选项

1/MDd,/MLd或者/MTd

  调试版本的运行时刻库有调试符号,使用了调试堆,调试堆的目的是发现内存破坏和内存泄漏,并且向用户报告源代码的哪个地方出了问题。特性:

.调试版本的运行时刻库对内存的分配作了跟踪,允许用户检查内存泄漏。

.在刚分配的内存里写上0xCD的字节模式,用0xCD来填充刚分配的内存,有助于发现数据未被初始化的错误。

.在被释放的内存写上0xDD的字节模式,有助于发现已被释放的内存。

.在缓冲区的两边分配了四字节的保护数据,并用0xFD的字节模式作初始化,来检查写内存的上溢出和下溢出。

.在每个内存分配的地方对源代码文件名和行号作了记录,有助于用户在源代码中对内存分配进行定位。

2/Od

  这个选项用来关闭优化开关。因为未被优化的代码直接对应于源代码,所以比优化后的代码更容易读懂。未被优化的代码编译和链接会更快,会有更短的调试周期。而由于优化,发布版本不见得会比调试版本运行得好,优化代码要求编译器做一些假设,去除冗余,但有时这个假设是错误的,并且去掉的冗余也有可能隐藏错误。如发布版本的帧指针(EBP寄存器)省略(FPO)隐藏了函数原型不匹配的错误;在同步异常模式(只能由throw语句抛出,编译器默认,由/GX编译选项设置)下,异常处理程序可能被优化掉,会阻止程序中的C++异常处理代码安全地捕获结构异常,在这种情况下,你必须使用异步异常模式(采取任何指令都会产生异常的机制,由/Eha编译选项设置)。

3/D “_DEBUG”

  打开条件编译调试代码开关。只有这个符号被定义,调试代码才会被编译,MFC使用_DEBUG符号来确定到底链接的是哪个版本的MFC类库。在调试版本中,内联默认情况下是被关闭的。

4/ZI

  创建编辑继续(Edit and Continue)的程序数据库。这个选项会打开/GF编译选项,/GF编译选项会消除重复字符串,并将字符串放到只读内存。编辑继续功能需要获取存储在PDB文件里的特殊信息来使得代码的修改对调试器有效。如果被修改文件对应的信息不在PDB文件里,编辑继续功能就不能进行,而且在调试过程中对代码的任何修改都会出现下面的提示信息“One or more files are out of date or do not exist.”

5/GZ

在调试版本中用来发现那些在发布版本里才发现的错误。其作用如下:

.0xCC模式初始化自动(本地)变量。

.在通过函数指针调用函数时,检查栈指针,确认是否有调用规则不匹配。

.在函数最后检查栈指针是否被改变。

6/Gm

  打开最小化重新链接开关,减少链接时间。

2、特别针对发布版本的编译选项

1/MD,/ML或者/MT

  使用发布版本的运行时刻库。

2/O1或者/O2

  打开优化开关,使得程序会最小或说速度会最快,优化器还可能发现代码中潜在的错误,而这些错误可能会被调试版本掩盖。

3/D “NDEBUG”

  关闭条件编译调试代码开关。

4/GF

  消除重复字符串并将它们放到只读内存中以避免被错误地修改。

5/Zi

创建包含调试符号的程序数据库。

  如果一个错误只发生在发布版本里,除非你是个汇编高手,否则你需要调试符号来提示你到底程序出现了什么问题,调试符号保存在程序的数据库文件(PDB)中。Visual C++AppWizard默认情况下没有为发布版本创建调试符号。为创建调试符号,打开工程设置对话框,选择Win32 Release,在C/C++标签里选择Common类,在调试信息里,如果是发布版本选择Program Database,如果是调试版本选择Program Database for Edit and Continue(编辑继续选项与优化链接不相容,不适于发布版本)。在Link标签里选择Debug类,然后选择Debug InfoMicrosoft format选项,最好不要选择Separate types选项,这样所有的调试信息才会被合并到单独的一个PDB文件中。对于发布版本,选择Link标签,在Project options对话框的最后加上“/OPT:REF”,这个选项使得不被引用的函数和数据不会出现在可执行文件中,避免了文件的无谓增大。对于调试版本不要使用这个选项,它会关闭增量链接(incremental linking)

二、Visual C++编辑器的设置菜单

  当你打开或新建一个包含至少一个工程的Workspace后,Visual C++Project菜单中的“Settings…”命令就变为有效,选择它或者按下热键Alt+F7后,便可调出工程设置对话框,这里面的选项将影响整个工程的建立和调试过程,因此很重要。

  在这个对话框中,左上方的下拉列表框用于选择一种工程配置,包括有Win32 DebugWin32 ReleaseAll Configurations(指前两种配置一起),某些选项在不同的工程配置中有不同的缺省值。左边的树形视图给出了当前工程所有的文件及分类情况。下面我们就以Win32 Debug为例来看看与工程有关的的四个主要选项卡的各自功能与含义(一共有十个选项卡):

1、 General选项卡

  这个选项卡比较简单,从上向下的第一个选项用于更改使用MFC类库的方式: DLL的方式或是静态连接。我们可以在两种方式之间进行切换。第二个选项用于指定在编译连接过程中生成的中间文件和输出文件的存放目录,对于调试版本来说,缺省的目录是工程下面的“Debug”子目录。第三个选项用于指定是否允许每种工程配置都有自己的文件依赖关系(主要指头文件),由于绝大多数工程的调试版本和发布版本都具有相同的文件依赖关系,所以通常不需要更改该选项。

2、 Debug选项卡

  Debug选项卡中是一些与调试有关的选项,由于选项比较多,它们被分成了几个类,我们可以从Category中选择不同的类别,选项卡就会切换显示出相应的选项。

  在General类别中,可以指定要调试的可执行文件名。另外三个选项可以指定用于调试的工作目录,开始调试时给程序传送的命令行参数,以及进行远程调试时可执行文件的路径。

3C/C++选项卡

  C/C++选项卡控制着Visual C++的编译器,其中的选项比较多。下面有一个Project Options编辑框,里面列出的各种命令开关将会在开始编译时作为命令行参数传送给Visual C++的编译器。这些命令开关会跟随其它选项改变而改变。

  在General类别中,Warning level用于指定编译器显示警告的级别,如果选中了Warnings as errors,那么显示的每一个警告都将会引起一个错误,这样在编译完毕后就无法启动连接器来进行连接。Optimizations用于设置代码优化方式,优化的目的主要有提高运行速度和减小程序体积两种,但有时候这两种目的是相互矛盾的。另外,在极少数情况下,不进行优化,程序能正常运行,打开了优化措施之后,程序却会出现一些莫名其妙的问题。其实这多半是程序中有潜在的错误,关闭优化措施往往只是暂时解决问题。Debug info用于指定编译器产生的调试信息的类型,为了使用Visual C++的即编即调功能,必须在这里选择生成“Program Database for Edit and Continue”类型的调试信息。Preprocessor definitions是一些预先定义的宏名。

  C++ Language类别中的选项涉及到了C++语言的一些高级特性,包括有成员指针的表示方式、异常处理、运行时类型信息,一般情况下都不用改变它们。Code Generation类别中的选项涉及如何生成目标代码,一般情况下保持缺省值即可。在Customize类别中,从上到下六个选项的含义分别为:是否禁止使用MicrosoftC++的扩展;是否允许函数级别的连接;是否消除重复的字符串;是否允许进行最小化的重建;是否允许递增编译方式;是否允许编译器在开始运行时向Output窗口中输出自己的版本信息。

  在Listing Files类别中,我们可以指定编译器生成浏览信息和列表文件(Listing file),前者可由浏览信息维护工具BSCMAKE生成浏览信息文件,后者则包含了C/C++源文件经过编译后对应的汇编指令。Optimizations类别允许我们对优化措施进行更细微的控制,选择了Customize后,便可以选择进行哪几项优化,在Inline function expansion中我们可以指定对内联函数的扩展方式。Precompiled Headers类别中是关于预编译头文件的一些选项,一般情况下都不用更改。Preprocessor类别中是关于预处理的一些选择。

4Link 选项卡

  Link选项卡控制着Visual C++的连接器。在General类别中,可以指定输出的文件名,以及一些在连接过程中需要使用的额外的库文件或目标文件,下边五个选项的含义分别为:生成调试信息;忽略所有缺省的库文件;允许递增连接方式(这种方式可以加快连接的速度);生成MAP文件;允许进行性能分析。在Customize中选中Use program database允许使用程序数据库。在Debug类别中,我们可以指定调试信息的类别是Microsoft的格式,还是COFF格式,或者两种都有,选中Separate types后连接器会把调试信息分开放在PDB文件中,这样连接起来会更快一些,但调试时速度却会慢一些。Input类别中是一些与输入库文件有关的选项,我们可以在这里指定使用或不使用某些库文件或目标文件。Output类别中则是一些与最终输出的可执行文件有关的选项,一般情况下都不用改变。

三、Visual C++调试工具

1、调试窗口

1)观察窗口(Watch)

  调试程序时,可使用观察窗口监视变量和表达式。

2)快速查看窗口(Quick watch)

  功能和观察窗口差不多。

3)变量窗口(Variables)

  变量窗口有三个标签:Auto标签显示了当前语句和前一条语句用到的变量,Locals标签显示当前函数的局部变量,this标签显示了this指针执行的对象。

4)寄存器窗口(Register)

  可以监视CPU的寄存器、标志值以及浮点堆栈

5)内存窗口(Memory)

  可显示从一特定地址开始的虚拟内存。Address框允许你指定从哪个虚拟内存地址开始显示。

6)调用栈窗口(Call stack)

  可显示引起当前源代码语句执行的一系列函数调用,当前函数在堆栈的顶端。

7)反汇编窗口(Disassembly)

  可查看编译器生成的对应于源代码的汇编指令。

2、调试符号

  程序数据库文件(.pdb)包含了Visual C++调试器所需的调试信息和程序信息。调试信息包含了变量的名字和类型、函数原型、源代码行号、类和结构的布局、FPO调试信息(重建堆栈帧)以及进行增量链接所需的信息。对于设置了Program Database for Edit and Continue选项的程序,PDB还要包含执行编辑继续功能所需的信息。

3、使用断点

  断点(BreakPoint)是运行你向调试器描述环境,并让调试器设置好程序状态的一种机制。如果没有断点,只有在程序里一步一步跟踪使用调试器。在Visual C++中,你可以设置三种类型的断点:代码定位断点、数据断点和消息断点。

四、提高调试器的查错能力

  尽量采用编译时刻检查而不是运行时刻检查。

1、使用最高的编译警告级别/W4

  象if(x=2)这样的语句,默认的警告级别为/W3时不显示任何信息,但改成最高警告级别/W4时则会出现“waning C4706:assignment within conditional expression”的警告。/W4能给出一些/W3所不能给的警告。

2、在调试版本中使用/GZ编译选项

  /GZ选项用来发现那些在发布版本里才发现的错误,包括未被初始化的自动(局部)变量、堆栈错误、不正确的函数原型等。

3、使用#pragma warning编译器指示

  你可以使用#pragma warning编译器指示来禁止整个程序、特定的头文件、特定的代码文件或是特定的某一行代码的特定警告,这看你把#pragma放在哪里。

4、使用没有警告的编译法则/WX

  这个编译选项把所有的警告当成错误来对待,只有在假警告被消除之后才能应用。有时编译警告可能是合理的,处理编译警告的核心是要发现错误,而不是抑制警告本身。这个法则对于大的程序开发小组来说很有帮助。最终目标是消除错误,而不是消除警告。

五、内存空间与分配

1、内存分配错误

  动态内存分配错误有两种基本类型:内存错误和内存泄漏。

1)内存错误

  当一个指针或者该指针所指向的内存单元成为无效单元,或者内存中分配的数据结构被破坏时,就会造成内存错误。指针未被初始化,指针被初始化为一个无效地址,指针被不小心错误地修改,在与指针相关联的内存区域被释放后使用该指针(这种指针被称为虚悬(dangling)指针),这些都会使指针变为无效指针。当通过一个错误指针或者虚悬指针对内存进行写入,或者将指针强制转换为不匹配的数据结构,又或者是写数据越界,内存自身也会遭到破坏。删除未被初始化的指针、删除非堆指针、多次删除同一指针或者覆盖一个指针的内部数据结构,都会造成内存分配系统错误。

2)内存泄漏

  内存泄漏在被动态分配的内存没有被释放时产生。有许多情况会导致内存泄漏,如没有在程序的全部执行路径中释放内存,没有在析构函数中释放所有的内存等。一个程序在崩溃之前可运行的时间越长,则导致崩溃的原因与内存泄漏的关系越大。

  Windows会在程序结束的时候将泄漏的内存收回,因此内存泄漏是个暂时性的问题。但为什么必须消除内存泄露呢?首先,内存泄漏往往会导致系统资源的泄漏。动态分配内存往往不仅仅代表一块存储区域,还代表了某些类型的系统资源,如文件、窗口、设备上下文、GDI对象等。其次,高质量的程序和特定的服务器程序必须能够无限地运行下去。最后,内存泄漏往往是其他程序错误或不良编程习惯的征兆。

  导致内参泄漏的原因:忘记释放内存;构造函数失败;存在内存泄漏的析构函数;存在内存泄漏的异常处理程序;多个返回语句;使用错误形式的delete

2、关于内存的初始化

  在调试版本里,堆里未被初始化的内存被0xCD字节模式填充,堆里释放的内存被0xDD字节模式填充。堆栈里被初始化的内存被0xCC字节模式填充。调试版本和发布版本里,未被初始化的全局内存都被初始化为0

3、内存虚拟地址空间

  Windows使用一组固定的范围来分割进程的4GB虚拟地址空间,因此有时可通过查看指针的返回值来判断指针是否有效。

1Windows2000虚拟地址空间划分

0~0XFFFF(64KB):不能用来检测空指针赋值(访问冲突)

0x10000(64KB)~0x7FFEFFFF(2GB-64KB)Win32进程私有的(非保留的),用于程序代码和数据

0x7FFF0000(2GB-64KB)~0x7FFFFFFF(2GB):不能用来防止覆盖OS分区(访问冲突)

0x800000000(2GB)~0xFFFFFFFF(4GB):为操作系统保留,不可访问(访问冲突)

2Windows2000虚拟地址空间使用

0x00030000~0x0012FFFF:线程栈

0x00130000~0x003FFFFF:堆(有时堆位于此处)

0x00400000~0x005FFFFF:可执行代码

0x00600000~0x0FFFFFFF:堆(有时堆位于此处)

0x10000000~0x5FFFFFFFApp DLLsMsvcrt.dllMfc42.dll

0x77000000~0xFFFFFFFFAdvapi32.dllComctl32.dllGdi32.dllKernel32.dllNtdll.dllRpcrt4.dllShell32.dllUser32.dll

其中,0x00400000是所有版本的Windows能使用的最低基地址。

六、一些调试技术

1、调试死循环

  使用Debug菜单下的Break命令。在Windows2000中,如果程序有输入请求,可以使用F12键中断程序,然后检查窗口的调用栈,或单步跟踪代码找到死循环的发生原因。

2、用Spy++调试与消息有关的问题

  调试消息的最好方案是使用Visual C++提供的Spy++工具。Spy++允许程序员查看窗口、消息、进程和线程。Spy++默认的消息输出:第一栏显示行号。第二栏显示接受消息的句柄。第三栏中的“S”表示消息是用SendMessage发出的,“P”代表消息是由PostMessage发出的,“R”是消息句柄的返回值。第四栏给出解码后的消息名,消息参数或返回值。

3、非常规方法

1)重新编连你的应用程序

  当你的程序表现出异常的或意外的行为,或者Visual C++编译器因为一个内部编译器错误而失败时,最好删除工程中的DebugRelease文件夹,从头开始重新进行编连。

2)重新启动Visual C++

  Visual C++有超强的能力,但编译器的某些特性也会引起奇怪的错误。如果你的程序表现得很奇怪,你可是试着清除所有的断点,关闭或隐藏观察窗口,检查工程设置对话框看最近做了什么修改,直至重新启动Visual C++以便消除由于Visual C++环境引起的异常行为。

3)重新启动Windows

  当你发现Windows或者其他程序表现出异常的或出人意料的行为时,就应该重新启动Windows,以消除操作系统给调试带来的干扰。

相关文章推荐

vc调试:使用MAP文件快速定位程序崩溃代码行

   作为程序员,平时最担心见到的事情就是程序发生了崩溃,无论是指针越界还是非法操作,都将给我们的应用系统 造成巨大的损失。但在一个大型系统的测试过程中,初期出现程序崩溃似乎成了不可避免的事。其实测试...

VC中用于调试程序的几个宏的使用技巧

VC中用于调试程序的几个宏的使用技巧 摘要:本文介绍了VC中用于调试程序的几个宏的使用技巧 标签:技巧  C  使用 一、TRACE宏   当选择了Debug目标,并且afx...

如何使用VC++调试程序 Debug版本与Release版本

1.如何在Release状态下进行调试 Project->Setting=>ProjectSetting对话框,选择Release状态。C/C++标签中的Category选General,Optim...
  • Sayesan
  • Sayesan
  • 2014年07月04日 16:32
  • 503

Windows程序调试系列: 使用VC++生成调试信息

Windows程序调试系列: 使用VC++生成调试信息 ZhangTao,zhangtao.it@gmail.com, 译自 “Generating debug information with V...
  • wzsy
  • wzsy
  • 2015年10月28日 15:21
  • 881

SICK西克变频器编码器调试程序 Hiperface接口指令详解 (小黄人软件)485模式VC调零设置零位 增加读出和保存所有数据

SICK西克变频器编码器调试程序 Hiperface接口指令详解 (小黄人软件)485模式

vc++6.0环境下如何调试DLL程序

2011-11-03 12:07:13 标签:休闲 DLL调试 职场 调试肯定在Debug模式中 由于DLL程序不能生成窗口等等原因不好调试,所以我们无法了解DLL程序的流程是怎...

VC++程序调试

1          前言      当程序的运行结果与程序员预想的不一样,如死机,计算值不正确,出现内存访问冲突等,就需要进行调试2          进行调试前的准备工作      因为程序调试是...

VC++6.0 环境下如何调试DLL程序

vc++6.0环境下如何调试DLL程序         调试肯定在Debug模式中 由于DLL程序不能生成窗口等等原因不好调试,所以我们无法了解DLL程序的流程是怎么样的。下面简单介绍一个如何...
  • zhu2695
  • zhu2695
  • 2013年06月25日 14:37
  • 777

vc++6.0环境下如何调试DLL程序

调试肯定在Debug模式中 由于DLL程序不能生成窗口等等原因不好调试,所以我们无法了解DLL程序的流程是怎么样的。下面简单介绍一个如何来调试DLL程序。 应该知道写DLL文件肯定是给某个...

vc6.0程序调试技术总结

vc6.0程序调试技术总结 学c语言,一直编写的都是小程序。但是在写单片机程序时,会出现一些问题,就需要程序的调试技术。在图书馆借了一本书《c语言程序设计实验教程》(西工大出版社),再结合网上一些网...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:在VC程序中使用调试语句
举报原因:
原因补充:

(最多只允许输入30个字)