簡單的Memory leak跟蹤(一) DEBUG_NEW方案

前言

C++編碼中Memory Leak是一個很討厭卻又揮之不去的話題,最近由於引入了GC,爲了驗證GC是否確實正常free了內存,於是先提供了一個內存分配的Tracer。

與分配器不同,分配器主要解決的是兩個問題:

1、性能,池式分配往往能提供比直接Virtual Allocation快得多的效能。據說這一原則在Vista後無效了,因爲微軟修改了VA的實現機制,只是聽說,沒有實際測試過。

2、碎片,避免大量散內存分配衝散了本身連續的內存,導致後面內存因爲沒有連續區塊而分配不出來。

我們的跟蹤器Tracer主要是想解決一個問題,就是啥時候分了內存,啥時候刪的,程序退出時刪除掉沒。


方案一:DEBUG_NEW方案

基本上,這個主題之前也有很多前輩都寫過了,這裏也沒有超越前輩們的什麼方案,只是自己做這個模塊時的心得和理解。

這個問題有兩個比較成型的方案,一個就是MFC的DEBUG_NEW方案,MAX SDK用的也是這個方案。


其實原理很簡單,我們只要能獲取到當前語句的文件名和行號,然後new的時候,我們讓我們的Tracer記錄一下當前的地址,並與文件名和行號綁定,然後,delete的時候,按照地址來去掉這條記錄即可。

實現起來如何實現呢?


這個問題無非是要解決兩個問題,首先,new這個東西,我們需要接管下來,接管下來後才能記錄我們自己的信息。

C++的operator new是可以有不同形式的重載的,比如:

void* operator new (size_tInSize, const char* InLogMsg)
{
    std::cout << InLogMsg <<std::endl;
    return ::malloc(InSize);
}


調用這個重載時,new就要這麼調了:

int* p = new ("我正在分配內存") int;

注意,new和operator new不是一回事兒,而提供了特殊形式的operator new後,需要相應地提供類似的operator delete,否則會有Level 1 warning。

對這個問題有興趣可見本文最後的補充1,它與本文的主題無關,暫時無視。


第二個問題是,我們如何獲知當前語句的文件和航好呢?

C++可以用下面的方法來取得當前語句所在的文件和行號:

std::cout << "當前文件爲:" <<__FILE__  <<"。當前行號爲:" <<  __LINE__<<std::endl;


準備工作齊活兒了,準備開始吧!

首先,我們需要提供一個Tracer來記錄文件名和行號這些信息:

class TracerFileLn
{
public:
    TracerFileLn& singleton();
private:
    struct _AllocInfo
    {
        const char*filename_;
        size_tfileLn_;
    };
    typedefstd::hash_map<qword, _AllocInfo>alloc_hash;
    alloc_hashallocInfoHash_;
public:
    void traceMalloc(void* InPtr, const char* InFilename, size_t InFileLn)
    {
        _AllocInfosAllocInfo = { InFilename, InFileLn };
        allocInfoHash_.insert( alloc_hash::pair((qword)InPtr, sAllocInfo) );
    }
    void traceFree(void* InPtr)
    {
        auto it = allocInfoHash_.find(InPtr);
        allocInfoHash_.erase(it);
    }
};

所以,我們能不能提供下面這個new的重載呢?

void* operator new(size_tInSize,const char* InFilename,size_t InFileLn)

然後,operator new這麼實現:

void* operator new(size_t InSize, const char* InFilename, size_t InFileLn)
{
    void* pPtr = ::malloc(InSize);
    TracerFileLn::singleton().traceMalloc(pPtr, InFilename, InFileLn);
    return pPtr;
}


然後,operator delete需要這麼實現:

void operator delete(void* InPtr)
{
    TracerFileLn::singleton().traceFree(InPtr);
}
void operator delete(void* InPtr, const char* InFilename,size_t InFileLn)
{
    TracerFileLn::singleton().traceFree(InPtr);
}


記得 new[] 和 delete[] 也要做相應的實現。

但這樣的話,咱們就不能再使用C++的原生new了,而是必須要用新的new。

int * pPtr = new(__FILE__,__LINE__)int;


所有使用new的地方都要這麼寫,太麻煩了?不過這難不倒我們,有宏呢,就跟MFC DEBUG_NEW那樣:

#define DEBUG_NEW  new (__FILE__,__LINE__)

#define new DEBUG_NEW

然後再用new 的地方實際上就會被替換爲new (__FILE__,__LINE__) 了。

int * pPtr = newint;

繼續這麼寫就可以,只是在這句話之前,必須要確保其前面有#define new DEBUG_NEW的宏聲明。


信息Trace下來了,程序結束後,只需要看hash裏還有哪些AllocInfo,一個個Dump出來就可以查到相應的內存泄露了。

看起來很方便吧大笑?方不方便,後面還會繼續展開,敬請期待。



補充1:

提供新格式的operator new後,爲何要提供相應的operator delete呢?
因爲C++標準規定,object的構造是可以有異常的,如果構造有異常,那麼當前object就應該被回收。如果你對這個object的new使用了自定義格式,那麼在構造函數異常時,C++回收object也會使用對應自定義格式的delete,所以相應的operator delete一定要提供,否則這種情況下內存就不會回收了。\

但如果一切正常,手動調用delete時,調用的是哪個delete呢?
答案是標準的delete:
void operator delete(void* InPtr);

另外,new 和 operator new 不是一回事兒?
是的,new是C++關鍵字,new做的事情是什麼呢?
1,調用對應形式的operator new,分配內存。
2,調用placement new,也就是對象的構造函數,構造對象。
也就是new有兩步,第一步是operator new,operator new只管分配內存,不管別的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章