VC++ 6.0 中如何使用 CRT 调试功能来检测内存泄漏 程序员
做者:JerryZexpress
#define _CRTDBG_MAP_ALLOC #include<stdlib.h> #include<crtdbg.h> #include "debug_new.h"
MSDN 如是说:“必须保证上面声明的顺序,若是改变了顺序,可能不能正常工做。”至于这是为何,咱们不得而知。MS 的老大们常常这样故弄玄虚。
针对非 MFC 程序,再加上周星星的头文件:debug_new.h,固然若是不加这一句,也能检测出内存泄漏,可是你没法肯定在哪一个源程序文件中发生泄漏。Output 输出只告诉你在 crtsdb.h 中的某个地方有内存泄漏。我测试时 REG_DEBUG_NEW 没有起做用。加不加这个宏均可以检测出发生内存分配泄漏的文件。
其次,一旦添加了上面的声明,你就能够经过在程序中加入下面的代码来报告内存泄漏信息了:
编程
_CrtDumpMemoryLeaks();这就这么简单。我在周星星的例子代码中加入这些机关后,在 VC++ 调试会话(按 F5 调试运行) Output 窗口的 Debug 页便看到了预期的内存泄漏 dump。该 dump 形式以下:
Detected memory leaks! Dumping objects -> c:\Program Files\...\include\crtdbg.h(552) : {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 c:\Program Files\...\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long. Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD c:\Program Files\...\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long. Data: < C > E8 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 Object dump complete.
更具体的细节请参考本文附带的源代码文件。
下面是我看过 MSDN 资料后,针对“如何使用 CRT 调试功能来检测内存泄漏?”的问题进行了一番编译和整理,但愿对你们有用。若是你的英文很棒,那就不用往下看了,建议直接去读 MSDN 库中的技术原文。
C/C++ 编程语言的最强大功能之一即是其动态分配和释放内存,可是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程当中,动态分配的内存处理不当是最多见的问题。其中,最难捉摸也最难检测的错误之一就是内存泄漏,即未能正确释放之前分配的内存的错误。偶尔发生的少许内存泄漏可能不会引发咱们的注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各类 各样的征兆:从性能不良(而且逐渐下降)到内存彻底耗尽。更糟的是,泄漏的程序可能会用掉太多内存,致使另一个程序垮掉,而使用户无从查找问题的真正根源。此外,即便无害的内存泄漏也可能殃及池鱼。
幸运的是,Visual Studio 调试器和 C 运行时 (CRT) 库为咱们提供了检测和识别内存泄漏的有效方法。下面请和我一块儿分享收获——如何使用 CRT 调试功能来检测内存泄漏?windows
如何启用内存泄漏检测机制?
VC++ IDE 的默认状态是没有启用内存泄漏检测机制的,也就是说即便某段代码有内存泄漏,调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息。你必须设定两个最基本的机关来启用内存泄漏检测机制。
一是使用调试堆函数:数组
#define _CRTDBG_MAP_ALLOC #include<stdlib.h> #include<crtdbg.h>
注意:#include 语句的顺序。若是更改此顺序,所使用的函数可能没法正确工做。
经过包含 crtdbg.h 头文件,能够将 malloc 和 free 函数映射到其“调试”版本 _malloc_dbg 和 _free_dbg,这些函数会跟踪内存分配和释放。此映射只在调试(Debug)版本(也就是要定义 _DEBUG)中有效。发行版本(Release)使用普通的 malloc 和 free 函数。
#define 语句将 CRT 堆函数的基础版本映射到对应的“调试”版本。该语句不是必须的,但若是没有该语句,那么有关内存泄漏的信息会不全。
二是在须要检测内存泄漏的地方添加下面这条语句来输出内存泄漏信息:多线程
_CrtDumpMemoryLeaks();当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在 Output 窗口的 Debug 页中显示内存泄漏信息。好比:
Detected memory leaks! Dumping objects -> C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long. Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long. Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00 Object dump complete.
若是不使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏的输出是这样的:编程语言
Detected memory leaks! Dumping objects -> {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 {44} normal block at 0x00441BD0, 33 bytes long. Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD {43} normal block at 0x00441C20, 40 bytes long. Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 Object dump complete.根据这段输出信息,你没法知道在哪一个源程序文件里发生了内存泄漏。下面咱们来研究一下输出信息的格式。第一行和第二行没有什么可说的,从第三行开始:
xx}:花括弧内的数字是内存分配序号,本文例子中是 {45},{44},{43}; block:内存块的类型,经常使用的有三种:normal(普通)、client(客户端)或 CRT(运行时);本文例子中是:normal block; 用十六进制格式表示的内存位置,如:at 0x00441BA0 等; 以字节为单位表示的内存块的大小,如:32 bytes long; 前 16 字节的内容(也是用十六进制格式表示),如:Data: <AB> 41 42 等;
仔细观察不难发现,若是定义了 _CRTDBG_MAP_ALLOC ,那么在内存分配序号前面还会显示在其中分配泄漏内存的文件名,以及文件名后括号中的数字表示发生泄漏的代码行号,好比:ide
C:\Temp\memleak\memleak.cpp(15)双击 Output 窗口中此文件名所在的输出行,即可跳到源程序文件分配该内存的代码行(也能够选中该行,而后按 F4,效果同样) ,这样一来咱们就很容易定位内存泄漏是在哪里发生的了,所以,_CRTDBG_MAP_ALLOC 的做用显而易见。
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
这条语句不管程序在什么地方退出都会自动调用 _CrtDumpMemoryLeaks。注意:这里必须同时设置两个位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
设置 CRT 报告模式
默认状况下,_CrtDumpMemoryLeaks 将内存泄漏信息 dump 到 Output 窗口的 Debug 页, 若是你想将这个输出定向到别的地方,可使用 _CrtSetReportMode 进行重置。若是你使用某个库,它可能将输出定向到另外一位置。此时,只要使用如下语句将输出位置设回 Output 窗口便可:函数
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
有关使用 _CrtSetReportMode 的详细信息,请参考 MSDN 库关于 _CrtSetReportMode 的描述。
解释内存块类型
前面已经说过,内存泄漏报告中把每一块泄漏的内存分为 normal(普通块)、client(客户端块)和 CRT 块。事实上,须要留心和注意的也就是 normal 和 client,即普通块和客户端块。工具
除了上述的类型外,还有下面这两种类型的内存块,它们不会出如今内存泄漏报告中:
如何在内存分配序号处设置断点?
在内存泄漏报告中,的文件名和行号可告诉分配泄漏的内存的代码位置,但仅仅依赖这些信息来了解完整的泄漏缘由是不够的。由于一个程序在运行时,一段分配内存的代码可能会被调用不少次,只要有一次调用后没有释放内存就会致使内存泄漏。为了肯定是哪些内存没有被释放,不只要知道泄漏的内存是在哪里分配的,还要知道泄漏产生的条件。这时内存分配序号就显得特别有用——这个序号就是文件名和行号以后的花括弧里的那个数字。
例如,在本文例子代码的输出信息中,“45”是内存分配序号,意思是泄漏的内存是你程序中分配的第四十五个内存块:
Detected memory leaks! Dumping objects -> C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long. Data: <AB> 41 42 ...... Object dump complete.
CRT 库对程序运行期间分配的全部内存块进行计数,包括由 CRT 库本身分配的内存和其它库(如 MFC)分配的内存。所以,分配序号为 N 的对象即为程序中分配的第 N 个对象,但不必定是代码分配的第 N 个对象。(大多数状况下并不是如此。)
这样的话,你即可以利用分配序号在分配内存的位置设置一个断点。方法是在程序起始附近设置一个位置断点。当程序在该点中断时,能够从 QuickWatch(快速监视)对话框或 Watch(监视)窗口设置一个内存分配断点:
例如,在 Watch 窗口中,在 Name 栏键入下面的表达式:
_crtBreakAlloc
若是要使用 CRT 库的多线程 DLL 版本(/MD 选项),那么必须包含上下文操做符,像这样:
{,,msvcrtd.dll}_crtBreakAlloc
如今按下回车键,调试器将计算该值并把结果放入 Value 栏。若是没有在内存分配点设置任何断点,该值将为 –1。
用你想要在其位置中断的内存分配的分配序号替换 Value 栏中的值。例如输入 45。这样就会在分配序号为 45 的地方中断。
在所感兴趣的内存分配处设置断点后,能够继续调试。这时,运行程序时必定要当心,要保证内存块分配的顺序不会改变。当程序在指定的内存分配处中断时,能够查看 Call Stack(调用堆栈)窗口和其它调试器信息以肯定分配内存时的状况。若是必要,能够从该点继续执行程序,以查看对象发生了什么状况,或许能够肯定未正确释放对象的缘由。
尽管一般在调试器中设置内存分配断点更方便,但若是愿意,也可在代码中设置这些断点。为了在代码中设置一个内存分配断点,能够增长这样一行(对于第四十五个内存分配):
_crtBreakAlloc = 45;
你还可使用有相同效果的 _CrtSetBreakAlloc 函数:
_CrtSetBreakAlloc(45);
如何比较内存状态?
定位内存泄漏的另外一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类型 _CrtMemState。你能够用它来存储内存状态的快照:
_CrtMemState s1, s2, s3;
若要获取给定点的内存状态快照,能够向 _CrtMemCheckpoint 函数传递一个 _CrtMemState 结构。该函数用当前内存状态的快照填充此结构:
_CrtMemCheckpoint( &s1 );
经过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构,能够在任意地方 dump 该结构的内容:
_CrtMemDumpStatistics( &s1 );
该函数输出以下格式的 dump 内存分配信息:
0 bytes in 0 Free Blocks. 75 bytes in 3 Normal Blocks. 5037 bytes in 41 CRT Blocks. 0 bytes in 0 Ignore Blocks. 0 bytes in 0 Client Blocks. Largest number used: 5308 bytes. Total allocations: 7559 bytes.
若要肯定某段代码中是否发生了内存泄漏,能够经过获取该段代码以前和以后的内存状态快照,而后使用 _CrtMemDifference 比较这两个状态:
_CrtMemCheckpoint( &s1 );// 获取第一个内存状态快照 // 在这里进行内存分配 _CrtMemCheckpoint( &s2 );// 获取第二个内存状态快照 // 比较两个内存快照的差别 if ( _CrtMemDifference( &s3, &s1, &s2) ) _CrtMemDumpStatistics( &s3 );// dump 差别结果
顾名思义,_CrtMemDifference 比较两个内存状态(前两个参数),生成这两个状态之间差别的结果(第三个参数)。在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用 _CrtMemDifference 比较结果,是检查内存泄漏的另外一种方法。若是检测到泄漏,则可使用 _CrtMemCheckpoint 调用经过二进制搜索技术来分割程序和定位泄漏。
结论
尽管 VC ++ 具备一套专门调试 MFC 应用程序的机制,但本文上述讨论的内存分配很简单,没有涉及到 MFC 对象,因此这些内容一样也适用于 MFC 程序。在 MSDN 库中能够找到不少有关 VC++ 调试方面的资料,若是你能善用 MSDN 库,相信用不了多少时间你就有可能成为调试高手。
本人水平不高,谬误在所不免,请你们拍砖,不要客气。顺祝你们圣诞快乐!
JerryZ 于 2004 年平安夜,
调试方法和技巧
做者:非凡
便于调试的代码风格:
VC++编译选项:
调试方法:
一、使用 Assert(原则:尽可能简单)
assert只在debug下生效,release下不会被编译。
例子:
char* strcpy(char* dest,char* source) { assert(source!=0); assert(dest!=0); char* returnstring = dest; while((*dest++ = *source++)!= ‘\0’) { ; } return returnstring; }
二、防护性的编程
例子:
char* strcpy(char* dest,char* source) { if(source == 0) { assert(false); reutrn 0; } if(dest == 0) { assert(false); return 0; } char* returnstring = dest; while((*dest++ = *source++)!= ‘\0’) { ; } return returnstring; }
三、使用Trace
如下的例子只能在debug中显示,
例子:
a)、TRACE
CString csTest = “test”; TRACE(“CString is %s\n”,csTest);
b)、ATLTRACE
c)、afxDump
CTime time = CTime::GetCurrentTime(); #ifdef _DEBUG afxDump << time << “\n”; #endif
四、用GetLastError来检测返回值,经过获得错误代码来分析错误缘由
五、把错误信息记录到文件中
异常处理
程序设计时必定要考虑到异常如何处理,当错误发生后,不该简单的报告错误并退出程序,应当尽量的想办法恢复到出错前的状态或者让程序从头开始运行,而且对于某些错误,应该可以容错,即容许错误的存在,可是程序仍是可以正常完成任务。
调试技巧
一、VC++中F5进行调试运行
a)、在output Debug窗口中能够看到用TRACE打印的信息
b)、 Call Stack窗口中能看到程序的调用堆栈
二、当Debug版本运行时发生崩溃,选择retry进行调试,经过看Call Stack分析出错的位置及缘由
三、使用映射文件调试
a)、建立映射文件:Project settings中link项,选中Generate mapfile,输出程序代码地址:/MAPINFO: LINES,获得引出序号:/MAPINFO: EXPORTS。
b)、程序发布时,应该把全部模块的映射文件都存档。
c)、查看映射文件:见” 经过崩溃地址找出源代码的出错行”文件。
四、能够调试的Release版本
Project settings中C++项的Debug Info选择为Program Database,Link项的Debug中选择Debug Info和Microsoft format。
五、查看API的错误码,在watch窗口输入@err能够查看或者@err,hr,其中”,hr”表示错误码的说明。
六、Set Next Statement:该功能能够直接跳转到指定的代码行执行,通常用来测试异常处理的代码。
七、调试内存变量的变化:当内存发生变化时停下来。
常见错误
一、在函数返回的时候程序崩溃的缘由
a)、写自动变量越界
b)、函数原型不匹配
二、MFC
a)、使用错误的函数原型处理用户定义消息
正确的函数原型为:
afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam);
三、谨慎使用TerminateThread:使用TerminateThread会形成资源泄漏,不到万不得已,不要使用。
四、使用_beginthreadex,不要使用Create Thread来常见线程。
参考资料:
《Windows程序调试》
功能强大的vc6调试器
做者:yy2better
要成为一位优秀的软件工程师,调试能力必不可缺。本文将较详细介绍VC6调试器的主要用法。
windows平台的调试器主要分为两大类:
1 用户模式(user-mode)调试器:它们都基于win32 Debugging API,有使用方便的界面,主要用于调试用户模式下的应用程序。这类调试器包括Visual C++调试器、WinDBG、BoundChecker、Borland C++ Builder调试器、NTSD等。
2 内核模式(kernel-mode)调试器:内核调试器位于CPU和操做系统之间,一旦启动,操做系统也会停止运行,主要用于调试驱动程序或用户模式调试器不易调试的程序。这类调试器包括WDEB38六、WinDBG和softice等。其中WinDBG和softice也能够调试用户模式代码。
国外一位调试高手曾说,他70%调试时间是在用VC++,其他时间是使用WinDBG和softice。毕竟,调试用户模式代码,VC6调试器的效率是很是高的。所以,我将首先在本篇介绍VC6调试器的主要用法,其余调试器的用法及一些调试技能在后续文章中阐述。
一 位置断点(Location Breakpoint)
你们最经常使用的断点是普通的位置断点,在源程序的某一行按F9就设置了一个位置断点。但对于不少问题,这种朴素的断点做用有限。譬以下面这段代码:
void CForDebugDlg::OnOK() { for (int i = 0; i < 1000; i++) //A { int k = i * 10 - 2; //B SendTo(k); //C int tmp = DoSome(i); //D int j = i / tmp; //E } }
执行此函数,程序崩溃于E行,发现此时tmp为0,假设tmp本不该该为0,怎么这个时候为0呢?因此最好可以跟踪这次循环时DoSome函数是如何运行的,但因为是在循环体内,若是在E行设置断点,可能须要按F5(GO)许屡次。这样手要不停的按,很痛苦。使用VC6断点修饰条件就能够轻易解决此问题。步骤以下。
1 Ctrl+B打开断点设置框,以下图:
Figure 1设置高级位置断点
2 而后选择D行所在的断点,而后点击condition按钮,在弹出对话框的最下面一个编辑框中输入一个很大数目,具体视应用而定,这里1000就够了。
3 按F5从新运行程序,程序中断。Ctrl+B打开断点框,发现此断点后跟随一串说明:...487 times remaining。意思是还剩下487次没有执行,那就是说执行到513(1000-487)次时候出错的。所以,咱们按步骤2所讲,更改此断点的skip次数,将1000改成513。
4 再次从新运行程序,程序执行了513次循环,而后自动停在断点处。这时,咱们就能够仔细查看DoSome是如何返回0的。这样,你就避免了手指的痛苦,节省了时间。
再看位置断点其余修饰条件。如Figure 1所示,在“Enter the expression to be evaluated:”下面,能够输入一些条件,当这些条件知足时,断点才启动。譬如,刚才的程序,咱们须要i为100时程序停下来,咱们就能够输入在编辑框中输入“i==100”。
另外,若是在此编辑框中若是只输入变量名称,则变量发生改变时,断点才会启动。这对检测一个变量什么时候被修改很方便,特别对一些大程序。
用好位置断点的修饰条件,能够大大方便解决某些问题。
二 数据断点(Data Breakpoint)
软件调试过程当中,有时会发现一些数据会莫名其妙的被修改掉(如一些数组的越界写致使覆盖了另外的变量),找出何处代码致使这块内存被更改是一件棘手的事情(若是没有调试器的帮助)。恰当运用数据断点能够快速帮你定位什么时候何处这个数据被修改。譬以下面一段程序:
#include "stdafx.h" #includeint main(int argc, char* argv[]) { char szName1[10]; char szName2[4]; strcpy(szName1,"shenzhen"); printf("%s\n", szName1); //A strcpy(szName2, "vckbase"); //B printf("%s\n", szName1); printf("%s\n", szName2); return 0; }
这段程序的输出是
szName1: shenzhen szName1: ase szName2: vckbase
szName1什么时候被修改呢?由于没有明显的修改szName1代码。咱们能够首先在A行设置普通断点,F5运行程序,程序停在A行。而后咱们再设置一个数据断点。以下图:
Figure 2 数据断点
F5继续运行,程序停在B行,说明B处代码修改了szName1。B处明明没有修改szName1呀?但调试器指明是这一行,通常不会错,因此仍是静下心来看看程序,哦,你发现了:szName2只有4个字节,而strcpy了7个字节,因此覆写了szName1。
数据断点不仅是对变量改变有效,还能够设置变量是否等于某个值。譬如,你能够将Figure 2中红圈处改成条件”szName2[0]==''''y''''“,那么当szName2第一个字符为y时断点就会启动。
能够看出,数据断点相对位置断点一个很大的区别是不用明确指明在哪一行代码设置断点。
三 其余
1 在call stack窗口中设置断点,选择某个函数,按F9设置一个断点。这样能够从深层次的函数调用中迅速返回到须要的函数。
2 Set Next StateMent命令(debug过程当中,右键菜单中的命令)
此命令的做用是将程序的指令指针(EIP)指向不一样的代码行。譬如,你正在调试上面那段代码,运行在A行,但你不肯意运行B行和C行代码,这时,你就能够在D行,右键,而后“Set Next StateMent”。调试器就不会执行B、C行。只要在同一函数内,此指令就能够随意跳前或跳后执行。灵活使用此功能能够大量节省调试时间。
3 watch窗口
watch窗口支持丰富的数据格式化功能。如输入0x65,u,则在右栏显示101。
实时显示windows API调用的错误:在左栏输入@err,hr。
在watch窗口中调用函数。提醒一下,调用完函数后立刻在watch窗口中清除它,不然,单步调试时每一步调试器都会调用此函数。
4 messages断点不怎么实用。基本上能够用前面讲述的断点代替。
总结 调试最重要的仍是你要思考,要猜想你的程序可能出错的地方,而后运用你的调试器来证明你的猜想。而熟练使用上面这些技巧无疑会加快这个过程。最后,你们若是有关于调试方面的问题,我乐意参与探讨。