Windows平台下的内存泄漏检测

在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时将结点从链表中删除,这样只要链表中存在的都是未被删除的;或者使用数组,当有一个被删除,将这个位置的索引用队列的方式记录下来,每当要新增数组数据时根据队列中保存的索引找到对应的位置进行覆盖操作。这样可以节省一定的空间。

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏恰同学骚年

Entity Framework 基础知识走马观花

  (1)通过选择以XML方式打开edmx文件,我们可以可以清楚地看到,edmx模型文件本质就是一个XML文件;

1102
来自专栏SDNLAB

Open vSwitch系列之openflow版本兼容

众所周知Open vSwitch支持的openflow版本从1.0到1.5版本(当前Open vSwitch版本是2.3.2)通过阅读代码,处理openflow...

55613
来自专栏猿天地

用aop加redis实现通用接口缓存

系统在高并发场景下,最有用的三个方法是缓存,限流,降级。 缓存就是其中之一,目前缓存基本上是用redis或者memcached。 redis和memcached...

3377
来自专栏坚毅的PHP

my php & mysql FAQ

php中文字符串长度及定长截取问题使用str_len("中国") 结果为6,php系统默认一个中文字符长度为3,可改用mb_strlen函数获得长度,mb_su...

3826
来自专栏安恒网络空间安全讲武堂

PWNCTF部分复现

根据readData和writedata函数的逻辑发现数组是char [22][12],主要是判断越界的if语句有逻辑漏洞

1792
来自专栏Java架构师学习

带你深入了解Java线程中的那些事

引言 说到Thread大家都很熟悉,我们平常写并发代码的时候都会接触到,那么我们来看看下面这段代码是如何初始化以及执行的呢? public class Thre...

3248
来自专栏大内老A

ASP.NET MVC Controller激活系统详解:总体设计

我们将整个ASP.NET MVC框架划分为若干个子系统,那么针对请求上下文激活目标Controller对象的子系统被我们成为Controller激活系统。在正式...

2176
来自专栏Java 技术分享

JavaWeb 之文件的上传下载

5155
来自专栏Elasticsearch实验室

Elasitcsearch 底层系列 Lucene 内核解析之 Doc Value

       Elasticsearch 支持行存和列存,行存用于以文档为单位顺序存储多个文档的原始内容,在 Elasitcsearch 底层系列 Lucene...

3425
来自专栏蘑菇先生的技术笔记

AutoMapper使用手册(一)

2374

扫码关注云+社区

领取腾讯云代金券