C++探测内存泄漏

由于C++没有垃圾回收机制,在写C++代码的时候,很重要的一点就是要小心内存的使用,虽然大家都会格外注意new、delete和malloc、free要成对出现,delete完指针还要置为空,但是不免在大型项目中会遗忘,或者是调用第三方API内部有new却忘了释放。在Linux gcc下可以使用Valgrind工具来探测泄漏,本文重点介绍下在windows下如何探测内存泄漏。

使用CRT中的_CrtDumpMemoryLeaks探测内存泄漏

C++程序的内存分配要通过CRT(C Runtime Library)在运行时动态的分配,通过比较在分配内存和释放内存时的记录数据就可确定是否发生了内存泄漏,Visual Studio为Windows平台提供了检测和识别内存泄漏的工具_CrtDumpMemoryLeaks

在程序开头加上以下语句,注意include的顺序不要改变

#define _CRTDBG_MAP_ALLOC 
#include <stdlib.h>
#include <crtdbg.h>

在程序的出口处加上_CrtDumpMemoryLeaks(),在程序运行结束后会在输出窗口显示内存泄漏信息,注意__CrtDumpMemoryLeaks只有在debug模式下才会输出内存泄漏信息,下面以一个例子来展示一下

#define _CRTDBG_MAP_ALLOC 
#include <stdlib.h>
#include <crtdbg.h>
int main()
{
char* c = (char*)malloc(20 * sizeof(char)); // malloc
_CrtDumpMemoryLeaks();
return 0;
}

在输出窗口可以看到内存泄漏的信息

Detected memory leaks!
Dumping objects ->
C:\Users\Administrator\Desktop\Test\MemoryLeak\MemoryLeak\MemoryLeak.cpp(13) : {79} normal block at 0x00766680, 20 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

_CrtDumpMemoryLeaks()会记录下内存泄漏发生的文件及位置,还能告诉你有多少比特的内存被泄漏了,但是实际使用起来呢还是不太方便,这时候免费专业的VLD(Visual Leak Detector)就闪亮登场了。

使用VLD(Visual Leak Detector)探测内存泄漏

VLD是一个在VS中很好的探测内存泄漏的工具,官方对其的描述为

  • 为每个泄漏块提供完整的堆栈跟踪,包括源文件和行号信息(如果可用)
  • 检测大多数(如果不是全部)进程内存泄漏类型,包括基于COM的泄漏和纯Win32堆泄漏
  • 可以从泄漏检测中排除所选模块(DLL或甚至主EXE)
  • 提供泄漏块的完整数据转储(十六进制和ASCII)
  • 可自定义的内存泄漏报告:可以保存到文件或发送到调试器,并且可以包含可变级别的详细信息

安装VLD

  1. 进入VLD的主页:http://vld.codeplex.com/

  2. 进入主页后,点击【download】按钮进行下载 VLD 最新版本

  3. 如果VS是VS2015及以下版本,就可以直接使用VLD了,需要配置以下信息(根据VLD的安装目录进行调整)

  • C/C++ -> 常规 -> 附加包含目录:D:\Program Files\Visual Leak Detector\include
  • 链接 -> 常规 -> 附加库目录:D:\Program Files\Visual Leak Detector\lib\Win32

在cpp文件开头引用#include “vld.h”,如果有预编译头文件stdafx.h,则需要放到stdafx.h之后

如果VS是VS2015以上版本,安装完VLD会将VLD的配置文件vld.ini加到环境变量中,我以VS2019为例,介绍下VLD在VS高版本下的编译和使用

在VS2019中编译VLD

为了能够在VS2019中使用VLD,需要手动编译VLD的源码,首先下载VLD的源码:https://github.com/KindDragon/vld

用VS2019打开vld_vs14.sln工程,将除了VLD和Libs下的libformat项目以外的其他项目都暂时卸载掉,右键libformat项目属性菜单,将平台工具集和目标平台版本改为VS2019所支持的设置,我改的为v142和10.0

接下来进入vld.cpp,搜索#error Not supported VS,将以下三行代码注释掉后生成解决方案

#if _MSC_VER > 1924
#error Not supported VS
#endif

将vld/src/bin/Win32/Release-v142(根据你的编译设置)中的vld.lib和vld_x86.dll加进你需要探测内存泄漏的项目中,将vld源码中的vld.h和vld_def.h复制到项目中,将vld源码中setup下dbghelp中的dbghelp.dll和Microsoft.DTfW.DHL.manifest复制到项目运行目录下(debug文件下),添加好所有的头文件包含和lib包含就可以使用VLD了

在VS2019中使用VLD

在cpp文件开头引用#include “vld.h”,如果有预编译头文件stdafx.h,则需要放到stdafx.h之后,使用方法很简单,示例程序如下

#include <iostream>
#include "vld.h"
int main()
{
char* c = (char*)malloc(20 * sizeof(char)); // malloc
return 0;
}

在输出窗口可以看到以下信息

WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x00D58AF8: 20 bytes ----------
Leak Hash: 0x801B9582, Count: 1, Total 20 bytes
Call Stack (TID 36480):
ucrtbased.dll!malloc()
C:\Users\Administrator\Desktop\test\MemoryLeak\MemoryLeak\MemoryLeak.cpp (13): MemoryLeak.exe!main() + 0xA bytes
D:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl (78): MemoryLeak.exe!invoke_main() + 0x2D bytes
D:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl (288): MemoryLeak.exe!__scrt_common_main_seh() + 0x5 bytes
D:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl (331): MemoryLeak.exe!__scrt_common_main()
D:\agent\_work\4\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp (17): MemoryLeak.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x19 bytes
ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xE4 bytes
ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xB4 bytes
Data:
CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 췍췍췍췍췍췍췍췍
CD CD CD CD 췍췍......

Visual Leak Detector detected 1 memory leak (56 bytes).
Largest number used: 56 bytes.
Total allocations: 56 bytes.
Visual Leak Detector is now exiting.

相较于_CrtDumpMemoryLeaks(),VLD的输出信息更加友好,并且调用堆栈可以点击直接跳到内存泄漏的语句处,非常方便

使用VLD中可能会遇到的问题

问题一:不显示文件及行号

我在使用VLD的时候有时候Call Stack不显示具体的调用堆栈信息,这对探测内存泄漏位置造成了很大的困扰,相信很多小伙伴都遇到过,这种情况下主要是由于项目路径中存在中文,_CrtDumpMemoryLeaks()和VLD都默认编码格式为ascii,VLD可以通过修改vld.ini来解决这个问题

进入环境变量目录下的vld.ini文件,将ReportEncoding 设置为unicode

; Sets the type of encoding to use for the generated memory leak report. This
; option is really only useful in conjuction with sending the report to a file.
; Sending a Unicode encoded report to the debugger is not useful because the
; debugger cannot display Unicode characters. Using Unicode encoding might be
; useful if the data contained in leaked blocks is likely to consist of Unicode
; text.
;
; Valid Values: ascii, unicode
; Default: ascii
;
ReportEncoding = unicode

问题二:未生成内存泄漏的日志

如果想将内存泄露信息保存在日志文件中的话,则需要将vld.ini文件中ReportTo 设置为both,具体可以看前面的注释

; Sets the report destination to either a file, the debugger, or both. If
; reporting to file is enabled, the report is sent to the file specified by the
; ReportFile option.
;
; Valid Values: debugger, file, both
; Default: debugger
;
ReportTo = both
Author: Hongyi Guo
Link: https://guohongyi.com/2020/09/19/C-探测内存泄漏/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.