前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >(转载)VC的内存泄漏检查

(转载)VC的内存泄漏检查

作者头像
大菊观
发布2019-08-29 10:43:40
1.3K0
发布2019-08-29 10:43:40
举报

原文链接:https://cloud.tencent.com/developer/article/1494987

原文链接:https://cloud.tencent.com/developer/article/1494987

日期: 2016-12-20

参考:MSDN:ms-help://MS.MSDNQTR.v90.chs/dv_vsdebugnative/html/cf6dc7a6-cd12-4283-b1b6-ea53915f7ed1.htm

通过在MSDN中输入:DEBUG_NEW,可以找到“DEBUG_NEW 宏”,在"请参见其他资源"中,打开“MFC中的内存泄漏检测”,在“MFC中的内存泄漏检测”页面下方的相关章节中,可找到“检测和隔离内存泄漏”,它就是介绍如何使用 C 运行时库检测内存泄漏,链接就是上面那个链接。

动态分配和释放内存的功能是 C/C++ 编程的最强大功能之一,但最大的长处也可能成为最大的弱点。C/C++ 应用程序即是如此,在这些应用程序中,内存处理问题属于最常见的 bug。

幸运的是,Visual Studio 调试器和 C 运行时 (CRT) 库为您提供了检测和识别内存泄漏的有效方法。

这些方法不仅MFC程序可以使用,win32程序也可以使用。并且这些方法,不仅C++的内存分配方式(如new)可用,C的内存分配方式(如malloc)也可用。

零、原理

在VC中编写C/C++程序时,我们对new、malloc等的调用,在Debug模式下,最终都会调用_heap_alloc_dbg_impl。_heap_alloc_dbg_impl内部会真正分配内存,并且记录内存分配的文件名、行号、需要分配的内存大小及本次内存分配是整个程序第几次分配(在MSDN中叫“内存分配编号”,每调用_heap_alloc_dbg_impl一次,这个值加1,这个值在某些情况下可用来调试,非常重要。我们在程序入口处,调用:

_CrtSetBreakAlloc(111);

那么在第111次分配内存时,程序就会中断。

参考:ms-help://MS.MSDNQTR.v90.chs/dv_vsdebugnative/html/5d80a876-4540-4a2f-b935-e40fdfff4263.htm)。_heap_alloc_dbg_impl内部把这个记录保存在一个名为_CrtMemBlockHeader的结构体节点中,然后再把_CrtMemBlockHeader节点加入到双向链表_pFirstBlock中,_pFirstBlock是类型为_CrtMemBlockHeader的全局变量,定义为:

static _CrtMemBlockHeader * _pFirstBlock;

虽然我们不做任何设置,VC就可以检测内存泄漏,但通常情况下,由于在调用new、malloc分配内存时,并没有把分配内存的文件名及行号传递给_heap_alloc_dbg_impl,所以检测到的内存泄漏并不能准确定位,对于实际开发,意义不大。

本文重点要讨论的就是如何让new、malloc等内存分配方式,可以传文件名及行号给_heap_alloc_dbg_impl。

对于C++语言的分配方式,原理是通过重载new操作符,让new执行到带文件名和行号参数的operator new函数上(注意这里是函数)。

对于C语言的分配方式,原理是通过类似下面的宏定义,

#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)

把malloc(s),映射到对_malloc_dbg函数的调用。

一、对于Win32程序。

1.1 对于C分配的内存(本小节的内容是通用方法,MFC也是通过封装本小节的内容实现的)

Win32对C语言分配的内存进行泄漏检测是通过<crtdbg.h>文件中,对malloc等函数的重定义实现的。

在<crtdbg.h>文件中,有如下宏定义:

#ifdef _CRTDBG_MAP_ALLOC // 注意这个条件宏

#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)

// 其它C内存分配函数的宏定义,如calloc等

#endif /* _CRTDBG_MAP_ALLOC */

通过包括 crtdbg.h,将 malloc 和 free 函数映射到其“Debug”版本 _malloc_dbg 和 _free_dbg,这些函数将跟踪内存分配和释放。此映射只在调试版本(在其中定义了 _DEBUG)中发生。发布版本使用普通的 malloc 和 free 函数。

_malloc_dbg接收文件名、行号参数。_malloc_dbg最终会调用到_heap_alloc_dbg_impl,并把分配内存的文件名、行号传递给_heap_alloc_dbg_impl。

所以,要想检测C语言分配的内存泄漏,就要包含头文件<crtdbg.h>,并且在包含头文件前,定义宏_CRTDBG_MAP_ALLOC。

并非绝对需要该宏定义,但如果没有该宏定义,内存泄漏转储包含的有用信息将较少。这是因为当没有包含这个宏时,malloc函数只接收size_t nSize参数,不再包含文件名和行号。

MSDN文章:ms-help://MS.MSDNQTR.v90.chs/dv_vsdebugnative/html/43eedaa1-3f26-463c-9c31-ae21f10ed16d.htm推荐的头文件如下:

#define _CRTDBG_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

注:#include 语句必须采用上文所示顺序。如果更改了顺序,所使用的函数可能无法正确工作。

到这里,对malloc等的调用,都会被记录下来,但运行一遍程序,会发现,并没有打印任何泄漏信息。这是因为目前为止,只记录了内存分配,并没有输出信息。

输出记录的未释放内存,是通过调用_CrtDumpMemoryLeaks();实现。_CrtDumpMemoryLeaks的作用就是收集所有未释放的内存信息,并打印出来。所以,通常_CrtDumpMemoryLeaks要放到程序结束的位置。但一个程序往往有多个结束的位置,并且,_CrtDumpMemoryLeaks打印的是执行_CrtDumpMemoryLeaks的时候,未释放的内存。有些内存,往往在_CrtDumpMemoryLeaks之后释放,_CrtDumpMemoryLeaks仍然会报告出泄漏。

解决方法是:在程序开始处调用:

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

该语句在程序退出时自动调用 _CrtDumpMemoryLeaks,我们不需要再调用_CrtDumpMemoryLeaks打印报告了。

进阶篇:

在_CrtDumpMemoryLeaks中加断点发现,当调用了_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)之后,vc程序会在__DllMainCRTStartup函数中处理(dwReason == DLL_PROCESS_DETACH)分支时,调用_CRT_INIT,而_CRT_INIT内部,调用了_CrtDumpMemoryLeaks。

1.2 对于C++分配的内存

#ifdef _DEBUG

#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)

#endif

通过宏定义,把对new的调用,映射到带文件名和行号参数的operator new函数上,并最终调用到_heap_alloc_dbg_impl,并把分配内存的文件名、行号传递给_heap_alloc_dbg_impl。

注:_NORMAL_BLOCK也是在#include <crtdbg.h>中定义。并且C++分配的内存,也需要调用_CrtDumpMemoryLeaks打印报告(可通过程序入口出调用_CrtSetDbgFlag来避免对_CrtDumpMemoryLeaks的直接调用)。

二、对于MFC程序

MFC工程,最终也是按Win32工程方式的内存泄漏执行,不过MFC工程创建向导生成的MFC工程,自动支持C++分配的内存泄漏检测,我们不需要任何处理。

2.1 对于C++分配的内存

MFC是通过下面的语句支持的:

#ifdef _DEBUG

#define new DEBUG_NEW

#endif

我们简单分析一下:

在MFC中,DEBUG_NEW也是个宏,定义为:

#define DEBUG_NEW new(THIS_FILE, __LINE__)

所以,在MFC debug下,调用new操作符,会使用

void* __cdecl operator new(size_t nSize, LPCSTR lpszFileName, int nLine)

来分配内存,而operator new最终会调用到_heap_alloc_dbg_impl,并把分配内存的文件名、行号传递给_heap_alloc_dbg_impl。

当我们调用delete删除内存时,operator delete函数会最终执行_free_dbg_nolock,而_free_dbg_nolock内部,会把待删除的指针的记录,从_pFirstBlock链表中删除。

当程序结束时,_pFirstBlock会检测链表中未删除的内存,给出内存泄漏报告。

需要注意的是,并不是每个cpp文件中,都定义了DEBUG_NEW,尤其后添加的文件。

2.2 对于C分配的内存。

默认不显示文件名和行号。我们最终的目的,是让对malloc等函数的调用,调用到_malloc_dbg等函数上(因为只有_malloc_dbg才接收带文件名和行号的参数)。而<crtdbg.h>中,已把malloc映射到_malloc_dbg上了。但MFC程序,也做了对malloc的映射。所以,我们只要在MFC程序的stdafx.h文件中、在

#include <afxwin.h> // MFC core and standard components

之前,包含下面的代码即可:

#define _CRTDBG_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

注意,这些映射有_CRTDBG_MAP_ALLOC这个条件宏,所以要先定义它。

三、总结(这部分要全部看完再编写代码):

我们验证VC是否启动用了详细内存泄漏信息检测的方法,一个是运行一次程序,看报告是否包含文件名和行号;另一种方法是单步调试new或malloc调用,如果能进入带文件名和行号参数的函数,就表示打印报告时,可以输出文件名和等号。

Win32程序,检测c和C++内存泄漏的通知做法是:

a. 为了检测C语言内存泄漏,按顺序包含头文件(可放到stdafx.h文件中):

#define _CRTDBG_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

#define IUI_DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)

b. 为了检测C++内存,在每一个需要检测内存的cpp文件中,定义宏(位置在所有#include 之后):

#ifdef _DEBUG

#define new IUI_DEBUG_NEW

#endif // _DEBUG

c. 为了在程序结束时可以打印泄漏报告,在程序入口处调用:

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

MFC程序检测c和C++内存泄漏的通知做法是:

a. 在stdafx.h文件中、在

#include <afxwin.h> // MFC core and standard components

之前,包含下面的代码:

#define _CRTDBG_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

#define IUI_DEBUG_NEW new(__FILE__, __LINE__)

b. 在每一个需要检测内存的cpp文件中,定义宏(位置在所有#include 之后):

#ifdef _DEBUG

#define new IUI_DEBUG_NEW // 其实,只执行b步骤,并且,把IUI_DEBUG_NEW换成DEBUG_NEW也行。

#endif

如果放到#include <afxwin.h> 之前,会有编译错误。

之所以#define new DEBUG_NEW不放到stdafx.h文件中,是因为:定义的new,可能和gdiplus不兼容,因为gdiplus里,也重载了operator new,如果放到stdafx.h文件中,会比gdiplus的头文件先包含。导致编译gdiplus时错误。

另外,由于我们在cpp中,通常是第一个包含stdafx.h,之后还会包含其它头文件,而这些头文件,可能又重新定义了new,导致我们的new定义被覆盖。所以,安全的做法有两种:

一种是,专门做一个头文件,如DumpMemoryLeaks.h,内容如下:

#pragma once

#ifdef _DEBUG

#define new DEBUG_NEW

#endif

然后在每个cpp中,包含了所有头文件之后,包含

#include "DumpMemoryLeaks.h"

另一种是直接在每个cpp中包含了所有头文件之后,直接宏定义:

#ifdef _DEBUG

#define new DEBUG_NEW

#endif

我们推荐在cpp中直接使用宏定义,因为如果包含DumpMemoryLeaks.h,用户往往会在包含了DumpMemoryLeaks.h之后,再包含其它头文件。而我们希望的是,DumpMemoryLeaks.h在所有其它头文件包含之后再包含。但写成宏定义的话,很少人会在宏定义后再包含其它头文件。

四、思考:

  1. 如何做到在程序结束时,如果有内存泄漏,就弹出断言。
  2. 如何让检测到的泄漏报告中,包含分配时的调用栈

五,具体实施。

  1. 先用windows的搜索功能,搜索DEBUG_NEW,把所有包含DEBUG_NEW的cpp文件,都删除到回收站(一定要进回收站,方便改造完恢复。结合版本控件软件,做到可恢复双保险)
  2. 用RemoveFile.exe把漏网之鱼再删除到回收站。
  3. 用VS的查找功能,看是否仍然有漏网之鱼。如果有,手工删除。
  4. 用EMEditor的DumpMemoryLeaks.jsee宏,为剩余的CPP文件,插入#define new宏。

附:

DEBUG_NEW或自己定义的new宏,有可能与gdiplus的Gdiplus::GdiplusBase::operator new的冲突,在编译时,会收到编译错误:

error C2660: 'Gdiplus::GdiplusBase::operator new' : function does not take 3 arguments

解决方案有两种:

  1. 注释掉自己写的new宏或DEBUG_NEW,但这样,将失去内存泄漏输出详细信息的功能。
  2. 为gdi+提供重载的new和delete函数接受附加参数。下面是已写好的代码,把它放到一个新的头文件NewGdiplus.h中,然后包含NewGdiplus.h代替包含Gdiplus.h.
//// Ensure that GdiPlus header files work properly with MFC DEBUG_NEW and STL header files.

#define iterator _iterator

#ifdef _DEBUG

namespace Gdiplus
{
    namespace DllExports
    {
        #include <GdiplusMem.h>
    };

    #ifndef _GDIPLUSBASE_H
    #define _GDIPLUSBASE_H
    class GdiplusBase
    {
        public:
            void (operator delete)(void* in_pVoid)
            {
                DllExports::GdipFree(in_pVoid);
            }

            void* (operator new)(size_t in_size)
            {
                return DllExports::GdipAlloc(in_size);
            }

            void (operator delete[])(void* in_pVoid)
            {
                DllExports::GdipFree(in_pVoid);
            }

            void* (operator new[])(size_t in_size)
            {
                return DllExports::GdipAlloc(in_size);
            }

            void * (operator new)(size_t nSize, LPCSTR lpszFileName, int nLine)
            {
                return DllExports::GdipAlloc(nSize);
            }

            void operator delete(void* p, LPCSTR lpszFileName, int nLine)
            {
                DllExports::GdipFree(p);
            }

        };
    #endif // #ifndef _GDIPLUSBASE_H
}
#endif // #ifdef _DEBUG

#include <gdiplus.h>
#undef iterator
//// Ensure that Gdiplus.lib is linked.
#pragma comment(lib, "gdiplus.lib")

参考:https://support.microsoft.com/en-us/kb/317799

其它方法:

使用gflags.exe和windbg.exe提供了另一种检测内存泄漏的方法。


版权声明:本文为CSDN博主「psbeond」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。

原文链接:https://cloud.tencent.com/developer/article/1494987

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年08月14日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档