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時將結點從連結串列中刪除,這樣只要連結串列中存在的都是未被刪除的;或者使用陣列,當有一個被刪除,將這個位置的索引用佇列的方式記錄下來,每當要新增陣列資料時根據佇列中儲存的索引找到對應的位置進行覆蓋操作。這樣可以節省一定的空間。