Windows平台下的内存泄漏检测

时间:2022-01-04 07:44:30

在C/C++中内存泄漏是一个不可避免的问题,很多新手甚至有许多老手也会犯这样的错误,下面说明一下在windows平台下如何检测内存泄漏。

在windows平台下内存泄漏检测的原理大致如下。

1. 在分配内存的同时将内存块的信息保存到相应的结构中,标识为已分配

2. 当内存释放时在结构中查找,并将相应的标识设置为已释放

3. 在需要的位置调用HeapWalk,遍历整个堆内存,找到对应的内存块的首地址,并与定义的结构中的数据相匹配,根据结构中的标识判断是否释放,未释放的话给出相应的提示信息。

另外在VS系列的编译器中如果输出的调试信息的格式为:文件名(行号)双击这样的输出信息,会自动跳转到对应的位置,利用这点可以很容易的定位到未释放的内存的位置。

为了实现上述功能,我们使用重载new和delete的方式。下面是具体的代码:

#define MAX_BUFFER_SIZE 1000
typedef struct tag_ST_BLOCK_INFO
{
TCHAR m_szSourcePath[MAX_PATH];
INT m_iLine;
BOOL m_bDelete;
void *pBlock;
}ST_BLOCK_INFO, *LP_ST_BLOCK_INFO; class CMemoryLeak
{
public:
CMemoryLeak(void);
~CMemoryLeak(void);
void MemoryLeak();
void add(LPCTSTR m_szSourcePath, INT m_iLine, void *pBlock);
int GetLength();
ST_BLOCK_INFO& operator [](int nSite);
protected:
HANDLE m_heap;//自定义堆
LP_ST_BLOCK_INFO m_pBlockInfo;
int m_BlockSize; //当前缓冲区大小
int m_hasInfo;//当前记录了多少值
};
CMemoryLeak::CMemoryLeak(void)
{
if (m_heap == NULL)
{
//打开异常检测
m_heap = HeapCreate(HEAP_GENERATE_EXCEPTIONS,0,0);
ULONG HeapFragValue = 2;
//允许系统记录堆内存的使用
HeapSetInformation( m_heap,HeapCompatibilityInformation,&HeapFragValue ,sizeof(HeapFragValue)) ;
} if (NULL == m_pBlockInfo)
{
m_pBlockInfo = (LP_ST_BLOCK_INFO)HeapAlloc(m_heap, HEAP_ZERO_MEMORY, MAX_BUFFER_SIZE * sizeof(ST_BLOCK_INFO));
m_BlockSize = MAX_BUFFER_SIZE;
m_hasInfo = 0;
}
} void CMemoryLeak::add(LPCTSTR m_szSourcePath, INT m_iLine, void *pBlock)
{
//当前缓冲区已满
if (m_hasInfo >= m_BlockSize)
{
//扩大缓冲区容量
HeapReAlloc(m_heap, HEAP_ZERO_MEMORY, m_pBlockInfo, m_BlockSize * 2 * sizeof(ST_BLOCK_INFO));
m_BlockSize *= 2;
} m_pBlockInfo[m_hasInfo].m_bDelete = FALSE;
m_pBlockInfo[m_hasInfo].m_iLine = m_iLine;
_tcscpy(m_pBlockInfo[m_hasInfo].m_szSourcePath, m_szSourcePath);
m_pBlockInfo[m_hasInfo].pBlock = pBlock;
m_hasInfo++;
} CMemoryLeak::~CMemoryLeak(void)
{
HeapFree(m_heap, 0, m_pBlockInfo);
HeapDestroy(m_heap);
} void CMemoryLeak::MemoryLeak()
{
TCHAR pszOutPutInfo[2*MAX_PATH]; //调试字符串
BOOL bRecord = FALSE; //当前内存是否被记录
PROCESS_HEAP_ENTRY phe = {};
HeapLock(GetProcessHeap()); //检测时锁定堆防止对堆内存进行写入
OutputDebugString(_T("开始检查内存泄露情况.........\n")); while (HeapWalk(GetProcessHeap(), &phe))
{
//当这块内存正在使用时
if( PROCESS_HEAP_ENTRY_BUSY & phe.wFlags )
{
bRecord = FALSE;
for(UINT i = 0; i < m_hasInfo; i ++ )
{
if( phe.lpData == m_pBlockInfo[i].pBlock)
{
//校验这块内存是否被释放
if(!m_pBlockInfo[i].m_bDelete)
{
StringCchPrintf(pszOutPutInfo,2*MAX_PATH,_T("%s(%d):内存块(Point=0x%08X,Size=%u)\n")
,m_pBlockInfo[i].m_szSourcePath,m_pBlockInfo[i].m_iLine,phe.lpData,phe.cbData);
OutputDebugString(pszOutPutInfo);
}
bRecord = TRUE;
break;
}
}
if( !bRecord )
{
StringCchPrintf(pszOutPutInfo,2*MAX_PATH,_T("未记录的内存块(Point=0x%08X,Size=%u)\n")
,phe.lpData,phe.cbData);
OutputDebugString(pszOutPutInfo);
}
} } HeapUnlock(GetProcessHeap());
OutputDebugString(_T("内存泄露检查完毕.\n")); } int CMemoryLeak::GetLength()
{ return m_hasInfo;
} ST_BLOCK_INFO& CMemoryLeak::operator [](int nSite)
{
return m_pBlockInfo[nSite];
} CMemoryLeak g_MemoryLeak; void* __cdecl operator new(size_t nSize,LPCTSTR pszCppFile,int iLine)
{
//在分配内存的时候将这块内存信息记录到相应的结构中
void *p = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, nSize);
g_MemoryLeak.add(pszCppFile, iLine, p);
return p;
} void __cdecl operator delete(void *p, TCHAR *pstrPath, int nLine)
{
::operator delete(p);
HeapFree(GetProcessHeap(), 0, p);
} void __cdecl operator delete(void* p)
{
//依次遍历结构体数组,找到对应内存块的记录,将标志设置为已删除
for (int i = 0; i < g_MemoryLeak.GetLength(); i++)
{
if (p == g_MemoryLeak[i].pBlock)
{
g_MemoryLeak[i].m_bDelete = TRUE;
}
} HeapFree(GetProcessHeap(), 0, p);
}

下面是一个测试的例子

#ifdef _UNICODE
//将__FILE__转化为对应的UNICODE版本
#define GRS_WIDEN2(x) L ## x
#define GRS_WIDEN(x) GRS_WIDEN2(x)
#define __WFILE__ GRS_WIDEN(__FILE__)
//这段代码不能与重载的申明在同一个头文件下,否则在编译时会将定义的new函数进行替换
#define new new(__WFILE__,__LINE__)
#define delete(p) ::operator delete(p,__WFILE__,__LINE__)
#else
#define new new(__FILE__,__LINE__)
#define delete(p) ::operator delete(p,__FILE__,__LINE__)
#endif int _tmain()
{
int* pInt1 = new int;
int* pInt2 = new int;
float* pFloat1 = new float; BYTE* pBt = new BYTE[100]; delete[] pBt; //在DEBUG环境下启用检测
#ifdef _DEBUG
g_MemoryLeak.MemoryLeak();
#endif
return 0;
}

上面的代码中,定义了一个结构体 ST_BLOCK_INFO来保存每个分配的内存块的信息,同时采用数组的方式来保存多个内存块的信息,为了便于管理这些信息,专门定义了一个类来操作这个数组,类中记录了数组的首地址,当前保存的信息总量和当前能够容纳的信息总量,同时这个数组支持动态扩展。

在遍历时利用HeapWalk函数遍历系统默认堆中的所有内存,找到正在使用的内存,并在结构数组中查找判断内存是否被释放,如果未背释放则输出调试信息。在主函数中利用宏定义的方式,使程序只在debug环境下来校验内存泄漏,方便调试同时在发行时不会拖累程序运行。

最后对程序再做最后几点说明:

1. 动态数组不要使用new 和delete来分配和释放空间,因为我们重载了这两个函数,这样在检测的时候会有一定的影响

2. new本身的定义如下:



void* operator new(size_t size) throw(std::bad_alloc)



平时在使用上例如void p = new int 其实等于void *p = new(sizeof(int)),同时如果使用void *p = new int[10] 等于 void *p = new(sizeof(int) 10) 上面定义的#define new new(WFILE,LINE) 其实在调用时相当于void *p = new(WFILE,LINE) int,也就是等于void *p = new(sizeof(int), WFILE,LINE)当然delete也是同理

3. 在申请数组空间时不要使用系统默认的堆,因为重载new和delete使用的就是系统默认堆,检测的也是默认堆,如果用默认堆来保存数组数据,会对结果产生影响。

4. 当然用这样的方式写有点浪费内存资源,如果一个程序需要new出大量的数据,那么需要的额外内存也太多,所以可以使用链表来保存,当调用delete时将结点从链表中删除,这样只要链表中存在的都是未被删除的;或者使用数组,当有一个被删除,将这个位置的索引用队列的方式记录下来,每当要新增数组数据时根据队列中保存的索引找到对应的位置进行覆盖操作。这样可以节省一定的空间。