之前偶然发现,CRT检测内存泄漏的代码其实并不是特别复杂,就是用一个链表记录所有申请出来的内存,然后在程序退出时检测还有哪些内存未被释放。于是有了自制内存泄漏检测工具的想法。基本上是参考这篇文章和CRT源码写出来的:
C++不用工具,如何检测内存泄漏? - 知乎 (zhihu.com)
代码写的非常简陋,并没有经过实战检验,估计还有很多bug,但用来理解检测内存泄漏的原理已经大致够用了。另外,不可用于MFC程序,如果类重载了new运算符也不行,这个问题暂时解决不了。
链接:https://pan.baidu.com/s/15cqif7sZiN1P8HGacPofhQ
提取码:kqa7
第1步:定义双向链表,链表节点为clMemHeader对象,用来记录分配过的内存
typedef struct clMemHeader
{
struct clMemHeader* pBlockHeaderNext;
struct clMemHeader* pBlockHeaderPrev;
const char * szFileName;
int nLine;
size_t nDataSize;
} clMemHeader;
clMemHeader* pFirstHead = NULL;
clMemHeader* pLastHead = NULL;
第2步:重载new、new[]运算符:
void* operator new(size_t nSize, const char* lpszFileName, int nLine)
{
return alloc_mem(nSize, lpszFileName, nLine);
}
void* operator new[](size_t nSize, const char* lpszFileName, int nLine)
{
return alloc_mem(nSize, lpszFileName, nLine);
}
为了避免new里面又调用new导致循环递归,实际分配内存使用malloc,封装成函数alloc_mem。为了记录分配的内存,将其添加到全局链表中:
void* alloc_mem(size_t nSize, const char* lpszFileName, int nLine)
{
size_t nTotalSize = sizeof(clMemHeader) + nSize;
clMemHeader* pHead = (clMemHeader*)malloc(nTotalSize);
// 与CRT类似,向左增长,但注意链表头在最左边(CRT就这么写的,有点别扭)
if (pFirstHead)
{
pFirstHead->pBlockHeaderPrev = pHead;
}
else
{
pLastHead = pHead;
}
pHead->pBlockHeaderNext = pFirstHead;
pHead->pBlockHeaderPrev = NULL;
pHead->szFileName = lpszFileName;
pHead->nLine = nLine;
pHead->nDataSize = nSize;
// 链表头始终在最左边
pFirstHead = pHead;
void* pRet = ((clMemHeader*)pHead) + 1;
return pRet;
}
第3步:重载delete运算符
void operator delete(void* p)
{
free_mem(p);
}
// 与new参数列表要一致,否则报错C4291
void operator delete(void* p, const char* lpszFileName, int nLine)
{
free_mem(p);
}
同理,为了避免循环递归,释放内存用free,封装成函数free_mem。调用delete的时候将内存指针从全局链表中删掉:
void free_mem(void* pUserData)
{
clMemHeader* pHead = (clMemHeader*)((char*)pUserData - sizeof(clMemHeader));
// 从链表中删除节点
if (pHead->pBlockHeaderNext)
{
pHead->pBlockHeaderNext->pBlockHeaderPrev = pHead->pBlockHeaderPrev;
}
else
{
pLastHead = pHead->pBlockHeaderPrev;
}
if (pHead->pBlockHeaderPrev)
{
pHead->pBlockHeaderPrev->pBlockHeaderNext = pHead->pBlockHeaderNext;
}
else
{
pFirstHead = pHead->pBlockHeaderPrev;
}
// 释放
free(pHead);
}
第4步:检测内存泄漏
void _dumpMemLeak()
{
OutputDebugStringA("*************************************\r\n");
OutputDebugStringA("Detect memory leaks!\r\n");
char buf[1024] = {0};
for (clMemHeader* pHead = pFirstHead; pHead != NULL; pHead = pHead->pBlockHeaderNext)
{
memset(buf, 0, 1024);
sprintf_s(buf, 1024, "%s(%d) leak %d byte at %p. \r\n",
pHead->szFileName, pHead->nLine, pHead->nDataSize, ((clMemHeader*)pHead + 1));
OutputDebugStringA(buf);
}
OutputDebugStringA("*************************************\r\n");
}
在程序退出之前调用_dumpMemLeak,检测全部链表中节点的数量,如果节点数量不为0则有内存泄漏,将泄漏的内存打印出来。
实际使用时需要先定义宏:
// 强制让所有new都走重载的运算符
#define new new(__FILE__, __LINE__)
下面是测试代码,看看能否找出内存泄漏:
int main()
{
char* pBuf = new char[1024];
//delete[] pBuf;
// 无需在new中显式调用构造函数,编译器会自动生成调用构造函数的代码
CPerson* pPerson1 = new CPerson;
delete pPerson1;
pPerson1 = NULL;
CPerson* pPerson2 = new CPerson(100);
delete pPerson2;
pPerson2 = NULL;
// 编译器会自动计算实际需要的内存大小,并传给new[]
stPerson* pPersonArray = new stPerson[10];
//delete[] pPersonArray;
// 检测内存泄漏
_dumpMemLeak();
}
CPerson的定义,如果调用无参构造函数,不会泄漏,调用有参构造函数,则会内存泄漏:
class CPerson
{
public:
CPerson()
{
int a = 0;
}
CPerson(int nBufSize)
{
char* pBuf = new char[nBufSize];
}
};
stPerson的定义:
struct stPerson
{
int nID;
};
测试结果:
可以看到,内存泄漏的代码位置已经打印出来了。