VC的内存泄漏检查

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

本文链接:https://blog.csdn.net/psbeond/article/details/99546363

日期: 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提供了另一种检测内存泄漏的方法。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • HTML+CSS,PC端/手机端公用部分样式代码整理(自己收藏)

    我们一般都会提一个 .overflow {overflow:hidden} 方便布局,也可以有效的阻止移动端上下左右拖动问题(溢出问题)

    用户5997198
  • 学习路线

    我们可以通过今年最新的TIOBE编程语言排行榜看到,JAVA在“昨天”、和“今天”都强势霸据榜单第一名,哇哦,看起来好像很厉害,那么为我们又为什么要学习Java...

    BWH_Steven
  • 基础知识 | 每日一练(77)

    小林:在 PC 兼容的分段结构下, 很难透明地分配超过 640K 的内存, 尤其是在 MS-DOS 下。

    闫小林
  • OC代码规范1——多用类型常量,少用#define预处理指令

    两年前针对这一点写过一篇文章Effective Objective-C 2.0——多用类型常量,少用#define预处理指令,本文是在这篇文章的基础上进行扩展的...

    拉维
  • 基础知识 | 每日一练(79)

    士人有百折不回之真心,才有万变不穷之妙用。立业建功,事事要从实地着脚,若少慕声闻,便成伪果;讲道修德,念念要从虚处立基,若稍计功效,便落尘情。 ...

    闫小林
  • JVM类加载机制和双亲委派模型

    虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

    用户3467126
  • CSS基础知识

    CSS全称为“层叠样式表 (Cascading Style Sheets)”,它主要是用于定义HTML内容在浏览器内的显示样式,如文字大小、颜色、字体加粗等。使...

    公众号php_pachong
  • C++一种高精度计时器

    在windows下可以通过QueryPerformanceFrequency()和QueryPerformanceCounter()等系列函数来实现计时器的功能...

    charlee44
  • 11款适合移动设备使用CSS3分页导航条源码解析/代码下载

    所有的分页导航条DEMO的html结构都是一样的:使用一个<nav>元素来包裹一个无序列表。列表项中的.button是前一页和后一页按钮。

    用户5997198
  • C/C++创建多级目录

    C运行时库提供的创建目录的函数_mkdir(),在上级目录不存在时会创建失败。所以自己实现了一下创建多级目录,无论上级目录是否存在。

    charlee44

扫码关注云+社区

领取腾讯云代金券