如何檢測記憶體洩漏——過載new和delete

rabbit729發表於2010-12-11

版權申明
本文可以被自由轉載,但是必須遵循如下版權約定:
1、保留本約定,並保留在文章的開頭部分。
2、不能任意修改文章內容,或者刪節,增加。如果認為本文內容有不當之處需要修改,請
與作者聯絡。
3、不能摘抄本文的內容,必須全文發表或者引用。
4、必須保留作者署名、註明文章出處。(本文授權給www.linuxaid.com.cn)
5、如不遵守本規定,則無權轉載本文。 
作者
ariesram
電子郵件地址
ariesram@linuxaid.com.cn, 或 ariesram@may10.ca
本文及本人所有文章均收集在bambi.may10.ca/~ariesram/articles/中。
本文授權給www.linuxaid.com.cn。

正文:
我曾經參與過一個比較大的專案,在這個專案裡面,我們沒有一個完全確定的設計文件,所以程式的實現常常變動。雖然我們有一個比較靈活的框架,但是從程式的角度來講,它使我們的程式非常的混亂。直到釋出的日期臨近,我們還沒有一個穩定的可以用來做alpha測試的版本。所以我們必須儘快的刪除掉無用的程式碼,讓這個版本足夠的穩定。但是,在這個沒有足夠規範的軟體公司,我們沒有時間也沒有足夠的精力來做邊界測試之類的工作。所以我們只能採用變通的辦法。在軟體中最大的問題就是記憶體洩漏。因為往往出現這樣的情況,我們在一段程式碼中分配了記憶體,但是卻沒有釋放它。這造成了很大的問題。我們需要一個簡單的解決方案,能夠簡單的編譯進這個專案,在執行的時候,它能夠產生一個沒有被釋放的記憶體的列表,用這個列表,我們能夠改正程式的錯誤。這就是我們稱之為記憶體跟蹤的方法。首先,我們需要一種程式碼,能夠被加入到原始碼中去,而且這種程式碼能夠被重用。程式碼重用是一種很重要的特性,能夠節省大量的時間和金錢以及程式設計師的勞動。另外,我們的這種程式碼必須簡單,因為我們當時已經沒有那麼多的時間和精力去完全重看一遍所有的程式碼來重新編寫以及改正錯誤從而使記憶體跟蹤能夠起作用。

好在,我們總能夠找到解決的辦法。首先,我們檢查了程式碼,發現所有的程式碼都是用new來分配記憶體,用delete來釋放記憶體。那麼,我們能夠用一個全程替換,來替換掉所有的new和delete操作符嗎?不能。因為程式碼的規模太大了,那樣做除了浪費時間沒有別的任何好處。好在我們的原始碼是用C++來寫成的,所以,這意味著沒有必要替換掉所有的new和delete,而只用過載這兩個操作符。對了,值用過載這兩個操作符,我們就能在分配和釋放記憶體之前做點什麼。這是一個絕對的好訊息。我們也知道該如何去做。因為,MFC也是這麼做的。我們需要做的是:跟蹤所有的記憶體分配和互動引用以及記憶體釋放。我們的原始碼使用Visual C++寫成,當然這種解決方法也可以很輕鬆的使用在別的C++程式碼裡面。要做的第一件事情是過載new和delete操作符,它們將會在所有的程式碼中被使用到。我們在stdafx.h中,加入:
#ifdef _DEBUG
inline void * __cdecl operator new(unsigned int size, 
const char *file, int line)
{
};

inline void __cdecl operator delete(void *p)
{
};
#endif
這樣,我們就過載了new和delete操作符。我們用$ifdef和#endif來包住這兩個過載操作符,這樣,這兩個操作符就不會在釋出版本中出現。看一看這段程式碼,會發現,new操作符有三個引數,它們是,分配的記憶體大小,出現的檔名,和行號。這對於尋找記憶體洩漏是必需的和重要的。否則,就會需要很多時間去尋找它們出現的確切地方。加入了這段程式碼,我們的呼叫new()的程式碼仍然是指向只接受一個引數的new操作符,而不是這個接受三個引數的操作符。另外,我們也不想記錄所有的new操作符的語句去包含__FILE__和__LINE__引數。我們需要做的是自動的讓所有的接受一個引數的new操作符呼叫接受三個引數的new操作符。這一點可以用一點點小的技巧去做,例如下面的這一段巨集定義,
#ifdef _DEBUG
#define DEBUG_NEW new(__FILE__, __LINE__)
#else
#define DEBUG_NEW new
#endif
#define new DEBUG_NEW
現在我們所有的接受一個引數的new操作符都成為了接受三個引數的new操作符號,__FILE__和__LINE__被預編譯器自動的插入到其中了。然後,就是作實際的跟蹤了。我們需要加入一些例程到我們的過載的函式中去,讓它們能夠完成分配記憶體和釋放記憶體的工作。這樣來做, #ifdef _DEBUG
inline void * __cdecl operator new(unsigned int size,
const char *file, int line)
{
void *ptr = (void *)malloc(size);
AddTrack((DWORD)ptr, size, file, line);
return(ptr);
};
inline void __cdecl operator delete(void *p)
{
RemoveTrack((DWORD)p);
free(p);
};
#endif
另外,還需要用相同的方法來過載new[]和delete[]操作符。這裡就省略掉它們了。
最後,我們需要提供一套函式AddTrack()和RemoveTrack()。我用STL來維護儲存記憶體分配記錄的連線表。
這兩個函式如下:
typedef struct {
DWORD address;
DWORD size;
char file[64];
DWORD line;
} ALLOC_INFO;

typedef list<ALLOC_INFO*> AllocList;

AllocList *allocList;

void AddTrack(DWORD addr, DWORD asize, const char *fname, DWORD lnum)
{
ALLOC_INFO *info;

if(!allocList) {
allocList = new(AllocList);
}

info = new(ALLOC_INFO);
info->address = addr;
strncpy(info->file, fname, 63);
info->line = lnum;
info->size = asize;
allocList->insert(allocList->begin(), info);
};

void RemoveTrack(DWORD addr)
{
AllocList::iterator i;

if(!allocList)
return;
for(i = allocList->begin(); i != allocList->end(); i++)
{
if((*i)->address == addr)
{
allocList->remove((*i));
break;
}
}
};
現在,在我們的程式退出之前,allocList儲存了沒有被釋放的記憶體分配。為了看到它們是什麼和在哪裡被分配的,我們需要列印出allocList中的資料。我使用了Visual C++中的Output視窗來做這件事情。
void DumpUnfreed()
{
AllocList::iterator i;
DWORD totalSize = 0;
char buf[1024];

if(!allocList)
return;

for(i = allocList->begin(); i != allocList->end(); i++) {
sprintf(buf, "%-50s: LINE %d, ADDRESS %d %d unfreed ",
(*i)->file, (*i)->line, (*i)->address, (*i)->size);
OutputDebugString(buf);
totalSize += (*i)->size;
}
sprintf(buf, "----------------------------------------------------------- ");
OutputDebugString(buf);
sprintf(buf, "Total Unfreed: %d bytes ", totalSize);
OutputDebugString(buf);
};
現在我們就有了一個可以重用的程式碼,用來監測跟蹤所有的記憶體洩漏了。這段程式碼可以用來加入到所有的專案中去。雖然它不會讓你的程式看起來更好,但是起碼它能夠幫助你檢查錯誤,讓程式更加的穩定。
完整程式碼如下:

輸出結果:

testing own new operator...
e:/testcode/123/123/test.cpp         : LINE 99, ADDRESS 3762088 4 unfreed
Total Unfreed: 4 bytes
Total Unfreed: 0 bytes
-------------------------------
testing default new operator...
Total Unfreed: 0 bytes
Total Unfreed: 0 bytes
請按任意鍵繼續. . .

相關文章