學習打造自己的DEBUG_NEW


學習范例http://www.cppblog.com/Robertxiao/archive/2012/11/05/194547.html

在使用MFC庫開發程序時,我非常喜歡MFC框架中的內存泄漏診斷機制,它的確能很好地幫助我們查找出內存泄漏。可是鏈接了MFC庫也使得生成的可執行文件大了許多,這個沒什么負面影響。

最可怕的是如果僅為了使用內存診斷機制,而帶來了鏈接庫沖突的麻煩。我也是在遇到這個問題時,才寫出了一個簡易的內存診斷機制。

一般大家都誤以為這些內存泄漏的檢測功能是由MFC提供的,其實不然。MFC只是封裝和利用了MS C-Runtime Library的Debug Function。

非MFC程序也可以利用MS C-Runtime Library的Debug Function加入內存泄漏的檢測功能。MS C-Runtime Library在實現malloc/free,strdup等函數時已經內建了內存泄漏的檢測功能。

注意觀察一下由MFC Application Wizard生成的項目,在每一個cpp文件的頭部都有這樣一段宏定義:

#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

有了這樣的定義,在編譯DEBUG版時,出現在這個cpp文件中的所有new都被替換成DEBUG_NEW了。那么DEBUG_NEW是什么呢?DEBUG_NEW也是一個宏,以下摘自afx.h,1632行

#define DEBUG_NEW new(THIS_FILE, __LINE__)

所以如果有這樣一行代碼:

char* p = new char[200];

經過宏替換就變成了:

char* p = new( THIS_FILE, __LINE__)char[200];

根據C++的標准,對於以上的new的使用方法,編譯器會去找這樣定義的operator new:

void* operator new(size_t, LPCSTR, int)

我們在afxmem.cpp 63行找到了一個這樣的operator new 的實現

void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine)

{

 return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);

}

void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)

{

 …

 pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);

 if (pResult != NULL)

  return pResult;

 …

}

  第二個operator new函數比較長,為了簡單期間,我只摘錄了部分。很顯然最后的內存分配還是通過_malloc_dbg函數實現的,這個函數屬於MS C-Runtime Library 的Debug Function。這個函數不但要求傳入內存的大小,另外還有文件名和行號兩個參數。文件名和行號就是用來記錄此次分配是由哪一段代碼造成的。如果這塊內存在程序結束之前沒有被釋放,那么這些信息就會輸出到Debug窗口里。

  這里順便提一下THIS_FILE,__FILE和__LINE__。__FILE__和__LINE__都是編譯器定義的宏。當碰到__FILE__時,編譯器會把__FILE__替換成一個字符串,這個字符串就是當前在編譯的文件的路徑名。當碰到__LINE__時,編譯器會把__LINE__替換成一個數字,這個數字就是當前這行代碼的行號。在DEBUG_NEW的定義中沒有直接使用__FILE__,而是用了THIS_FILE,其目的是為了減小目標文件的大小。假設在某個cpp文件中有100處使用了new,如果直接使用__FILE__,那編譯器會產生100個常量字符串,這100個字符串都是飧?/SPAN>cpp文件的路徑名,顯然十分冗余。如果使用THIS_FILE,編譯器只會產生一個常量字符串,那100處new的調用使用的都是指向常量字符串的指針。

  再次觀察一下由MFC Application Wizard生成的項目,我們會發現在cpp文件中只對new做了映射,如果你在程序中直接使用malloc函數分配內存,調用malloc的文件名和行號是不會被記錄下來的。如果這塊內存發生了泄漏,MS C-Runtime Library仍然能檢測到,但是當輸出這塊內存塊的信息,不會包含分配它的的文件名和行號。

要在非MFC程序中打開內存泄漏的檢測功能非常容易,你只要在程序的入口處加入以下幾行代碼:

int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );

tmpFlag |= _CRTDBG_LEAK_CHECK_DF;

_CrtSetDbgFlag( tmpFlag );

  這樣,在程序結束的時候,也就是winmain,main或dllmain函數返回之后,如果還有內存塊沒有釋放,它們的信息會被打印到Debug窗口里。

如果你試着創建了一個非MFC應用程序,而且在程序的入口處加入了以上代碼,並且故意在程序中不釋放某些內存塊,你會在Debug窗口里看到以下的信息:

{47} normal block at 0x00C91C90, 200 bytes long.

Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

  內存泄漏的確檢測到了,但是和上面MFC程序的例子相比,缺少了文件名和行號。對於一個比較大的程序,沒有這些信息,解決問題將變得十分困難。

  為了能夠知道泄漏的內存塊是在哪里分配的,你需要實現類似MFC的映射功能,把new,maolloc等函數映射到_malloc_dbg函數上。這里我不再贅述,你可以參考MFC的源代碼。

  由於Debug Function實現在MS C-RuntimeLibrary中,所以它只能檢測到堆內存的泄漏,而且只限於malloc,realloc或strdup等分配的內存,而那些系統資源,比如HANDLE,GDI Object,或是不通過C-Runtime Library分配的內存,比如VARIANT,BSTR的泄漏,它是無法檢測到的,這是這種檢測法的一個重大的局限性。另外,為了能記錄內存塊是在哪里分配的,源代碼必須相應的配合,這在調試一些老的程序非常麻煩,畢竟修改源代碼不是一件省心的事,這是這種檢測法的另一個局限性。

 

//頭文件
#pragma once
#include <crtdbg.h>
#include <malloc.h>

#ifdef _DEBUG
#define THIS_FILE __FILE__
//添加宏定義,使用malloc分配內存也能檢測到內存泄漏
#define   malloc(s)   _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)

//設置使用new分配內存能檢測到內存泄漏
void* _cdecl operator new(size_t nSize, int nType, const char* lpszFileName, int nLine);

//覆蓋operator new 和 delete 運算符,
void* _cdecl operator new(size_t nSize, const char* lpszFileName, int nLine);
void* __cdecl operator new[](size_t nSize, const char* lpszFileName, int nLine);
void __cdecl operator delete(void* p);
void __cdecl operator delete[](void* p);

#define DEBUG_NEW new(THIS_FILE, __LINE__)
#else
#define DEBUG_NEW new
#endif  //_DEBUG


//mfc的afx.h頭文件中關於new覆蓋的定義
// Memory tracking allocation

//void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
//#define DEBUG_NEW new(THIS_FILE, __LINE__)
//void AFX_CDECL operator delete(void* p, LPCSTR lpszFileName, int nLine);
//
//void * __cdecl operator new[](size_t);
//void* __cdecl operator new[](size_t nSize, LPCSTR lpszFileName, int nLine);
//void __cdecl operator delete[](void* p, LPCSTR lpszFileName, int nLine);
//void __cdecl operator delete[](void *);
//實現文件
//#include "StdAfx.h"
#include <malloc.h>
#include "DebugNew.h"


namespace
{
    class AutoDetectMemory
    {
    public:
        AutoDetectMemory()
        {
#ifdef _DEBUG
            _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
            _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
        }
    };

    static AutoDetectMemory gs_am;

}


void* _cdecl operator new(size_t nSize, const char* lpszFileName, int nLine)
{
    return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}

void* __cdecl operator new[](size_t nSize, const char* lpszFileName, int nLine)
{
    return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}

void __cdecl operator delete(void* p)
{
#ifdef _DEBUG
    _free_dbg(p, _NORMAL_BLOCK);
#else
    free(p);
#endif
}

void __cdecl operator delete[](void* p)
{
    ::operator delete(p);
}

void* __cdecl operator new(size_t nSize, int nType, const char* lpszFileName, int nLine)
{
#ifdef _DEBUG
    return _malloc_dbg(nSize, nType, lpszFileName, nLine);
#else
    //UNREFERENCED_PARAMETER(nType);
    //UNREFERENCED_PARAMETER(lpszFileName);
    //UNREFERENCED_PARAMETER(nLine);
    return ::operator new(nSize);
#endif
}

測試結果:

 

使用方式:

在stdafx.h中包含頭文件

在要用診斷機制的文件中加入如下的代碼。
#ifdef _DEBUG
#define new DEBUG_NEW
#endif

這樣不管是new申請的內存泄漏,還是malloc申請的內存泄漏在程序正常退出后都會輸出在Debug窗口里。而且都會顯示完整文件路徑、行數、泄漏字節數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM