如何检查内存泄露问题


        简单说明了一下没有工具的情况如何运用VC库中的工具来检查代码的内存泄漏问题。

一: 内存泄漏

        内存泄漏是编程中常常见到的一个问题,内存泄漏往往会一种奇怪的方式来表现出来,基本上每个程序都表现出不同的方式。 但是一般最后的结果只有两个,一个是程序当掉,一个是系统内存不足。 还有一种就是比较介于中间的结果程序不会当,但是系统的反映时间明显降低,需要定时的Reboot才会正常。

        有一个很简单的办法来检查一个程序是否有内存泄漏。就是是用Windows的任务管理器(Task Manager)。运行程序,然后在任务管理器里面查看 “内存使用”和”虚拟内存大小”两项,当程序请求了它所需要的内存之后,如果虚拟内存还是持续的增长的话,就说明了这个程序有内存泄漏问题。 当然如果内存泄漏的数目非常的小,用这种方法可能要过很长时间才能看的出来。

        当然最简单的办法大概就是用CompuWare的BoundChecker 之类的工具来检测了,不过这些工具的价格对于个人来讲稍微有点奢侈了。

        如果是已经发布的程序,检查是否有内存泄漏是又费时又费力。所以内存泄漏应该在Code的生成过程就要时刻进行检查。


二: 原因
        
内存泄漏产生的原因一般是三种情况:

  1. 分配完内存之后忘了回收;
  2. 程序Code有问题,造成没有办法回收;
  3. 某些API函数操作不正确,造成内存泄漏。


    1.
内存忘记回收,这个是不应该的事情。但是也是在代码种很常见的问题。分配内存之后,用完之后,就一定要回收。如果不回收,那就造成了内存的泄漏,造成内存泄漏的Code如果被经常调用的话,那内存泄漏的数目就会越来越多的。从而影响整个系统的运行。比如下面的代码:

for (int =0;I<100;I++)
{
    Temp = new BYTE[100];
}

就会产生 100*100Byte的内存泄漏。

    2. 在某些时候,因为代码上写的有问题,会导致某些内存想回收都收不回来,比如下面的代码:

Temp1 = new BYTE[100];
Temp2 = new BYTE[100];
Temp2 = Temp1;

这样,Temp2的内存地址就丢掉了,而且永远都找不回了,这个时候Temp2的内存空间想回收都没有办法。

    3. API函数应用不当,在Windows提供API函数里面有一些特殊的API,比如FormatMessage。 如果你给它参数中有FORMAT_MESSAGE_ALLOCATE_BUFFER,它会在函数内部New一块内存Buffer出来。但是这个 buffer需要你调用LocalFree来释放。 如果你忘了,那就会产生内存泄漏。

三: 检查方法

        一般的内存泄漏检查的确是很困难,但是也不是完全没有办法。如果你用VC的库来写东西的话,那么很幸运的是,你已经有了很多检查内存泄漏的工具,只是你想不想用的问题了。Visual C++的Debug版本的C运行库(C Runtime Library)。它已经提供好些函数来帮助你诊断你的代码和跟踪内存泄漏。 而且最方便的地方是这些函数在Release版本中完全不起任何作用,这样就不会影响你的Release版本程序的运行效率。

        比如下面的例子里面,有一个明细的内存泄漏。当然如果只有这么几行代码的话,是很容易看出有内存泄漏的。但是想在成千上万行代码里面检查内存泄漏问题就不是那么容易了。

char * pstr = new char[5];
lstrcpy(pstr,"Memory leak");

        如果我们在Debug版本的Code里面对堆(Heap)进行了操作,包括malloc, free, calloc, realloc, new 和 delete可以利用VC Debug运行时库中堆Debug函数来做堆的完整性和安全性检查。比如上面的代码,lstrcpy的操作明显破坏了pstr的堆结构。使其溢出,并破坏了临近的数据。那我们可以在调用lstrcpy之后的代码里面加入 _CrtCheckMemory函数。_CrtCheckMemory函数发现前面的lstrcpy使得pstr的堆结构被破坏,会输出这样的报告:

emory check error at 0x00372FA5 = 0x79, should be 0xFD.
memory check error at 0x00372FA6 = 0x20, should be 0xFD.
memory check error at 0x00372FA7 = 0x6C, should be 0xFD.
memory check error at 0x00372FA8 = 0x65, should be 0xFD.
DAMAGE: after Normal block (#41) at 0x00372FA0.
Normal located at 0x00372FA0 is 5 bytes long.

        它告诉说 pstr的长度应该时5个Bytes,但是在5Bytes后面的几个Bytes也被非法改写了。提醒你产生了越界操作。_CrtCheckMemory 的返回值只有TRUE和FALSE,那么你可以用_ASSERTE()来报告出错信息。 上面的语句可以换成 _ASSERTE(_CrtCheckMemory()); 这样Debug版本的程序在运行的时候就会弹出一个警告对话框,这样就不用在运行时候一直盯着Output窗口看了。这个时候按Retry,就可以进入源代码调试了。看看问题到底出在哪里。

        其他类似的函数还有 _CrtDbgReport, _CrtDoForAllClientObjects, _CrtDumpMemoryLeaks,_CrtIsValidHeapPointer, _CrtIsMemoryBlock, _CrtIsValidPointer,_CrtMemCheckpoint, _CrtMemDifference, _CrtMemDumpAllObjectsSince, _CrtMemDumpStatistics, _CrtSetAllocHook, _CrtSetBreakAlloc, _CrtSetDbgFlag,_CrtSetDumpClient, _CrtSetReportFile, _CrtSetReportHook, _CrtSetReportMode

        这些函数全部都可以用来在Debug版本中检查内存的使用情况。具体怎么使用这些函数就不在这里说明了,各位可以去查查MSDN。在这些函数中用处比较大的,或者说使用率会比较高的函数是_CrtMemCheckpoint, 设置一个内存检查点。这个函数会取得当前内存的运行状态。 _CrtMemDifference 检查两种内存状态的异同。 _CrtMemDumpAllObjectsSince 从程序运行开始,或者从某个内存检查点开始Dump出堆中对象的信息。还有就是_CrtDumpMemoryLeaks当发生内存溢出的时候Dump出堆中的内存信息。 _CrtDumpMemoryLeaks一般都在有怀疑是内存泄漏的代码后面调用。比如下面的例子:

#include <windows.h>
#include <crtdbg.h>
void main()
{
char * pstr;
pstr = new char[5];
_CrtDumpMemoryLeaks();
}

输出:
Detected memory leaks! à提醒你,代码有内存泄漏.
Dumping objects ->
{44} normal block at 0x00372DB8, 5 bytes long.
Data: < > CD CD CD CD CD
Object dump complete.

        如果你双击包含行文件名的输出行,指针将会跳到源文件中内存被分配地方的行。当无法确定那些代码产生了内存泄漏的时候,我们就需要进行内存状态比较。在可疑的代码段的前后设置内存检查点,比较内存使用是否有可疑的变化。以确定内存是否有泄漏。为此要先定义三个_CrtMemState 对象来保存要比较的内存状态。两个是用来比较,一个用了保存前面两个之间的区别。

_CrtMemState Sh1,Sh2,Sh_Diff;
char *pstr1 = new char[100];
_CrtMemCheckPoint(&Sh1); ->
设置第一个内存检查点
char *pstr2 = new char[100];
_CrtMemCheckPoint(&Sh2); ->设置第二个内存检查点
_CrtMemDifference(&Sh_Diff, &Sh1, &Sh2); ->检查变化
_CrtMemDumpAllObjectsSince(&Sh_Diff); ->Dump变化

        如果你的程序中使用了MFC类库,那么内存泄漏的检查方法就相当的简单了。因为Debug版本的MFC本身就提供一部分的内存泄漏检查。 大部分的new 和delete没有配对使用而产生的内存泄漏,MFC都会产生报告。这个主要是因为MFC重载了Debug版本的new 和delete操作符, 并且对前面提到的API函数重新进行了包装。在MFC类库中检查内存泄漏的Class就叫 CMemoryState,它重新包装了了_CrtMemState,_CrtMemCheckPoint, _CrtMemDifference, _CrtMemDumpAllObjectsSince这些函数。并对于其他的函数提供了Afx开头的函数,供MFC程序使用。比如 AfxCheckMemory, AfxDumpMemoryLeaks 这些函数的基本用法同上面提到的差不多。 CMemoryState和相关的函数的定义都在Afx.h这个头文件中。 有个简单的办法可以跟踪到这些函数的声明。在VC中找到MFC程序代码中下面的代码, 一般都在X.cpp的开头部分

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

        把光标移到DEBUG_NEW上面 按F12,就可以进入Afx.h中定义这些Class和函数的代码部分。 VC中内存泄漏的常规检查办法主要是上面的两种。当然这两种方法只是针对于Debug版本的Heap的检查。如果Release版本中还有内存泄漏,那么检查起来就麻烦很多了。

4 .总结:

        实际上Heap的内存泄漏问题是相当的好查的。VC的提供的检查工具也不太少,但是如果是栈出了什么问题,恐怕就麻烦很多了。栈出问题,一般不会产生内存泄漏,但是你的代码的逻辑上很有可能会有影响。这个是最最痛苦的事情。 编程,就是小心,小心再小心而已。


******************************************************************************************************************************************************


MFC内存泄露检测



这几天一直在检查程序内存泄露的问题,今天终于告一段落。
内存泄露在编制小型应用程序时看不出其危害,但如果是编制24小时运行的大型平台应用程序时,如果有内存泄露,则随着程序的运行,其占用的内存会越来越多,最终导致系统崩溃。因此,内存泄露不容小觑。下面是几天来检查内存泄露的一点经验小结。

1.常规内存泄露的检测
常规内存泄露一般是由于编程者在手动申请内存空间之后没有释放造成的。如用new、malloc()函数或CString的GetBuuffer()函数申请内存空间后没有使用对应的释放语句释放内存空间。
(1)大范围的内存泄露检测
_CrtDumpMemoryLeaks()函数就是显示 当前的内存泄漏。注意是“当前”,也就是说当它执行时,所有未销毁的对象均会报内存泄漏,因此尽量让这条语句在程序的最后执行。用法如下:
在StdAfx.h中添加如下代码:

#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
#endif 

在需要检测当前未销毁内存的位置添加如下代码:

_CrtDumpMemoryLeaks();

则在debug模式下,程序运行到上述代码时,在debug的输出框会出现类似如下信息:

Detected memory leaks!
    Dumping objects ->
    D:\...\*****.cpp(1463) : {8244} normal block at 0x01AA84B8, 512 bytes long.
    Data: <                > 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

上述信息提示检测到内存泄露发生的具体的文件名及行号,后面则给出了内存泄露的标号、大小及内容。
(2)代码块的内存泄露检测
_CrtMemCheckpoint()、_CrtMemDifference()、_CrtMemDumpStatistics()函数可以实现代码块在执行前后的内存比较,从而实现对内存泄露语句的精确定位。用法如下:

_CrtMemState s1, s2, s3;
    ...
    _CrtMemCheckpoint(&s1);
    ...(需要检测的代码块)
   
_CrtMemCheckpoint(&s2);
    if(_CrtMemDifference(&s3, &s1, &s2))
        _CrtMemDumpStatistics(&s3);

这段代码在执行时,在_CrtMemCheckpoint(&s1)位置会保存一份当前的内存快照,_CrtMemCheckpoint(&s2)位置又会保存一份当前内存的快照,_CrtMemDifference(&s3, &s1, &s2)则会比较s1与s2内存快照的区别,并存入s3,如果发生内存泄露,_CrtMemDifference(&s3, &s1, &s2)会返回真值。_CrtMemDumpStatistics(&s3)则可以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. 

如果该代码块没有发生内存泄露,则不会出现上述信息。

2.第三方库内存泄露检测
有时候内存泄露并不是由于程序员未释放申请的内存空间,而是由于第三方库函数造成的内存泄露,这种内存泄露用常规方法很难发现。这时,可以先根据经验确定是哪一块代码发生了内存泄露,然后注释掉这部分代码及其之后的代码,运行程序,同时在“任务管理器”或其他内存管理工具中观察该程序运行时的内存变化情况,如果随时间增加,内存不再显著增加,则可确定发生内存泄露的位置应该在注释掉的代码中。然后去掉该段代码的注释,如果在“任务管理器”中该程序又出现明显的内存泄露,则可确定该段代码出现了内存泄露。通过这种方法,对程序逐块检验,逐层深入,就可以最终精确定位发生内存泄露的语句。
如果出现这种内存泄露,先检查是否是自己对该第三方库的使用不当造成的内存泄露,如果确定不是,则证明该第三方库的函数有缺陷,应向该库的开发者反映改进并换用其他途径解决该段代码设计问题。

3.多线程造成的内存泄露检测
多线程造成的内存泄露最难以检测,它通常是由于内存空间的申请和释放线程不同步造成的。举个例子:
假设在一个函数中有如下代码:

void ***::DisplayChart(...)
    {
        ...
        x=new double[100*100];
        y=new double[100*100];
        z=new double[100*100];
        ...
        Invalidate();
    }

在OnPaint函数中有如下代码:

void ***::OnPaint()
    {
        ...
        delete[] x;
        delete[] y;
        delete[] z;
        ...
    }

Invalidate()函数执行时会发送消息WM_PAINT,从而会导致OnPaint()函数的执行,这样看似没问题,但其实仍有可能造成内存泄露。
因为Invalidate()函数执行时只是发送WM_PAINT消息到消息队列,到OnPaint()函数的执行还有一段时间,在这个时间间隔中如果主程序再次调用DisplayChart()函数,则x、y、z的值会被新申请的内存空间的地址刷新,而原来OnPaint()函数中的delete还没来得及执行,这样就造成了内存泄露。
这种内存泄露问题很隐蔽,只能先找到发生内存泄露的大致位置,然后凭经验去分析出现内存泄露的原因,从而解决问题。
这种内存泄露的解决办法就是在多线程时加入一些线程同步和等待的代码,确保申请的内存空间能及时的释放。


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值