前言
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只管分配記憶體,不管別的。