之所以撰寫這篇文章是因為前段時間花費了很大的精力在已經成熟的代碼上再去處理memory leak問題。寫此的目的是希望我們應該養成良好的編碼習慣,盡可能的避免這樣的問題,因為當你對著一大片的代碼再去處理此類的問題,此時無疑增加了解決的成本和難度。準確的說屬于補救措施了。
1. 什么是內存泄漏(memory leak)?
指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費。
A memory leak is a particular type of unintentional memory consumption by a computer program where the program fails to release memory when no longer needed. This condition is normally the result of a bug in a program that prevents it from freeing up memory that it no longer needs.This term has the potential to be confusing, since memory is not physically lost from the computer. Rather, memory is allocated to a program, and that program subsequently loses the ability to access it due to program logic flaws.
2. 對于C和C++這種沒有Garbage Collection 的語言來講,我們主要關注兩種類型的內存泄漏:
堆內存泄漏(Heap leak)。對內存指的是程序運行中根據需要分配通過malloc,realloc new等從堆中分配的一塊內存,再是完成后必須通過調用對應的 free或者delete 刪掉。如果程序的設計的錯誤導致這部分內存沒有被釋放,那么此后這塊內存將不會被使用,就會產生Heap Leak.
系統資源泄露(Resource Leak).主要指程序使用系統分配的資源比如 Bitmap,handle ,SOCKET等沒有使用相應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能降低,系統運行不穩定。
3. 如何解決內存泄露?
內存泄露的問題其困難在于1.編譯器不能發現這些問題。2.運行時才能捕獲到這些錯誤,這些錯誤沒有明顯的癥狀,時隱時現。3.對于手機等終端開發用戶來說,尤為困難。下面從三個方面來解決內存泄露:
第一,良好的編碼習慣,盡量在涉及內存的程序段,檢測出內存泄露。當程式穩定之后,在來檢測內存泄露時,無疑增加了排除的困難和復雜度。
使用了內存分配的函數,要記得要使用其想用的函數釋放掉,一旦使用完畢。
Heap memory:
malloc\realloc ------ free
new \new[] ---------- delete \delete[]
GlobalAlloc------------GlobalFree
要特別注意數組對象的內存泄漏
MyPointEX *pointArray =new MyPointEX [100];
其刪除形式為:
delete []pointArray
Resource Leak :對于系統資源使用之前要仔細看起使用方法,防止錯誤使用或者忘記釋放掉系統資源。
我們看MSDN上一個創建字體的例子:

示例代碼
RECT rect;
HBRUSH hBrush;
FONT hFont;
hdc = BeginPaint(hWnd, &ps);
hFont = reateFont(48,0,0,0,FW_DONTCARE,FALSE,TRUE,FALSE,DEFAULT_CHARSET,OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS,CLEARTYPE_QUALITY, VARIABLE_PITCH,TEXT("Impact"));
SelectObject(hdc, hFont);
SetRect(&rect, 100,100,700,200);
SetTextColor(hdc, RGB(255,0,0));
DrawText(hdc, TEXT("Drawing Text with Impact"), -1,&rect, DT_NOCLIP);
DeleteObject(hFont);
EndPaint(hWnd, &
如果使用完成時候忘記釋放字體,就造成了資源泄漏。
對于基于引用計數的系統對象尤其要注意,因為只有其引用計數為0時,該對象才能正確被刪除。而其使用過程中有其生成的新的系統資源,使用完畢后,如果沒有及時刪除,都會影響其引用計數。

示例代碼
IDNS *m_pDns//define a DNS object.
If(NULL == m_pDns)
{
IEnv_CreateInstance (m_pEnv,AEECLSID_DNS,(void **) (&m_pDns))
}
If(m_pDns)
{
Char szbuff[256];
IDNS_AddQuestions(M_pDns,AEEDNSTYPE_A,ADDDNSCLASS_IN,szbuff);
IDNS_Start(m_pDns,this);
const AEEDNSResponse * pDnsResponse = NULL;
IDNS_GetResponse(pMe->m_pDns, &pDnsResponse);
…………………………………………………………
…………………………………………………………..
………………………………………………………..
DNS_Release(pMe->m_pDns);//當程序運行到此時,其返回值不是0,是1,其含義是程序已經產生內存泄露了,系統已經有一個由DNS所產生的內核對象沒有釋放,而當這段代碼多次執行之后,內存泄露將不斷增加……..
m_pDns=NULL;
}
看起來很不直觀,仔細分析就會發現,對象pDnsResponse是從m_pDns產生新的object,所以m_pDns的引用計數會增加,因此在使用完pDnsResponse,應該release 該對象使其引用計數恢復正常。
對于資源,也可使用RAII,RAII(Resource acquisition is initialization)資源獲取即初始化,它是一項很簡單的技術,利用C++對象生命周期的概念來控制程序的資源,例如內存,文件句柄,網絡連接以及審計追蹤(audit trail)等.RAII的基本技術原理很簡單.若希望保持對某個重要資源的跟蹤,那么創建一個對象,并將資源的生命周期和對象的生命周期相關聯.如此一來,就可以利用C++復雜老練的對象管理設施來管理資源.(有待完善)
例2:
Struct ITypeface *pTypeface;
if (pTypeface)
{
IANY_CreateInstance(g_pApplet->m_pIShell,AEECLSID_BTFETypeface,void**)& Typeface);
}
接下來我們就可以從這個接口上面創建字體,比如
IHFont **pihf=NULL;
ITypeface_NewFontFromFile(ITypeface,……,&pihf).
ITypeface_NewFontFrommemory(ITypeface,……..,&pihf)
ITypeface_NewFontFromClassID(IType,……,&pihf)
但是要切記,這些字體在使用完成后一定要release掉,否則最后 iTypeface的引用計數就是你最后沒有刪除掉的字體的個數。
第二,重載 new 和 delete。這也是大家編碼過程中常常使用的方法。
下面給出簡單的sample來說明。

示例代碼
memchecker.h
structMemIns
{
void * pMem;
int m_nSize;
char m_szFileName[256];
int m_nLine;
MemIns * pNext;
};
classMemManager
{
public:
MemManager();
~MemManager();
private:
MemIns *m_pMemInsHead;
int m_nTotal;
public:
static MemManager* GetInstance();
void Append(MemIns *pMemIns);
void Remove(void *ptr);
void Dump();
};
void *operatornew(size_tsize,constchar*szFile, int nLine);
void operatordelete(void*ptr,constchar*szFile, int nLine);
void operatordelete(void*ptr);
void*operatornew[] (size_tsize,constchar*szFile,int nLine);
void operatordelete[](void*ptr,constchar*szFile, int nLine);
void operatordelete[](void *ptr);
memechecker.cpp
#include"Memchecher.h"
#include<stdio.h>
#include<malloc.h>
#include<string.h>
MemManager::MemManager()
{
m_pMemInsHead=NULL;
m_nTotal=NULL;
}
MemManager::~MemManager()
{
}
voidMemManager::Append(MemIns *pMemIns)
{
pMemIns->pNext=m_pMemInsHead;
m_pMemInsHead = pMemIns;
m_nTotal+= m_pMemInsHead->m_nSize;
}
voidMemManager::Remove(void *ptr)
{
MemIns * pCur = m_pMemInsHead;
MemIns * pPrev = NULL;
while(pCur)
{
if(pCur->pMem ==ptr)
{
if(pPrev)
{
pPrev->pNext =pCur->pNext;
}
else
{
m_pMemInsHead =pCur->pNext;
}
m_nTotal-=pCur->m_nSize;
free(pCur);
break;
}
pPrev = pCur;
pCur = pCur->pNext;
}
}
voidMemManager::Dump()
{
MemIns * pp = m_pMemInsHead;
while(pp)
{
printf( "File is %s\n", pp->m_szFileName );
printf( "Size is %d\n", pp->m_nSize );
printf( "Line is %d\n", pp->m_nLine );
pp = pp->pNext;
}
}
voidPutEntry(void *ptr,intsize,constchar*szFile, int nLine)
{
MemIns * p = (MemIns *)(malloc(sizeof(MemIns)));
if(p)
{
strcpy(p->m_szFileName,szFile);
p->m_nLine = nLine;
p->pMem = ptr;
p->m_nSize = size;
MemManager::GetInstance()->Append(p);
}
}
voidRemoveEntry(void *ptr)
{
MemManager::GetInstance()->Remove(ptr);
}
void *operatornew(size_tsize,constchar*szFile, int nLine)
{
void * ptr = malloc(size);
PutEntry(ptr,size,szFile,nLine);
return ptr;
}
voidoperatordelete(void *ptr)
{
RemoveEntry(ptr);
free(ptr);
}
void operatordelete(void*ptr,constchar * file, intline)
{
RemoveEntry(ptr);
free(ptr);
}
void*operatornew[] (size_tsize,constchar* szFile,intnLine)
{
void * ptr = malloc(size);
PutEntry(ptr,size,szFile,nLine);
return ptr;
}
void operatordelete[](void *ptr)
{
RemoveEntry(ptr);
free(ptr);
}
void operatordelete[](void*ptr,constchar*szFile,intnLine)
{
RemoveEntry(ptr);
free(ptr);
}
#definenewnew(__FILE__,__LINE__)
MemManagerm_memTracer;
MemManager*MemManager::GetInstance()
{
return &m_memTracer;
}
void main()
{
int *plen =newint ;
*plen=10;
delete plen;
char *pstr=newchar[35];
strcpy(pstr,"hello memory leak");
m_memTracer.Dump();
return ;
其主要思路是將分配的內存以鏈表的形式自行管理,使用完畢之后從鏈表中刪除,程序結束時可檢查改鏈表,其中記錄了內存泄露的文件,所在文件的行數以及泄露的大小哦。
第三,Boost 中的smart pointer(待完善,結合大家的建議)
第四,一些常見的工具插件,詳見我的Blog中相關文章。
4. 由內存泄露引出內存溢出話題:
所謂內存溢出就是你要求分配的內存超出了系統能給你的,系統不能滿足需求,于是會產生內存溢出的問題。
常見的溢出主要有:
內存分配未成功,卻使用了它。 常用解決辦法是,在使用內存之前檢查指針是否為NULL。如果指針p 是函數的參數,那么在函數的入口處用assert(p!=NULL)進行檢查。如果是用malloc 或new 來申請內存,應該用if(p==NULL)或if(p!=NULL)進行防錯處理。
內存分配雖然成功,但是尚未初始化就引用它。 內存分配成功并且已經初始化,但操作越過了內存的邊界。 例如在使用數組時經常發生下標“多1”或者“少1”的操作。特別是在for 循環語句中,循環次數很容易搞錯,導致數組操作越界。
使用free 或delete 釋放了內存后,沒有將指針設置為NULL。導致產生“野指針”。
程序中的對象調用關系過于復雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。(這點可是深有感受,呵呵)
不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。
在windows下開發C++程序的時候,我們經常需要用到malloc開申請內存,然后利用free回收內存,但是開發人員的不小心可能會忘記free掉內存,這樣就導致了內存泄露
利用庫檢測內存泄露信息
#define _CRTDBG_MAP_ALLOC //如果沒有這個宏定義,我們只能知道有內存泄露,卻無法知道在哪個地方申請內存忘記了釋放
#include
<stdlib.h>
#include
<crtdbg.h>
int main(void)
{
char *p = (char *)malloc(sizeof(char) * 100);
_CrtDumpMemoryLeaks();
}
使用crtdbg來檢測到內存泄露很簡單,只要在文件的第一行定義_CRTDBG_MAP_ALLOC,然后include頭文件crtdbg.h,在程序需要內存檢測的地方調用_CrtDumpMemoryLeaks,就可以輸出內存泄露的信息,如上面的程序,我們申請了100個字節的內存而沒有釋放,但是我們可以很清楚地看到內存泄露在 哪個地方。
我們在main.cpp這個文件中的第八行申請了內存,但是沒有進行釋放
那么編譯器是怎么知道我們有內存泄露呢??就是利用宏定義把我們的調用的malloc替換成crtdbg 庫里面的_malloc_dbg,我們在申請內存的時候,_malloc_dbg會先記錄下我們申請內存的行數以及大小(記得編譯器有內置的宏定義__LINE__和__FILE__不?),把這些信息放到一個list(只是舉例,使用list保存這些信息一旦程序大了會很慢)里面,當我們free內存的時候,把這塊內存的信息從list里面刪除掉,我們調用_CrtDumpMemoryLeaks()的時候就是在把這個list的信息依次打印出來而已
下面是我們定義_CRTDBG_MAP_ALLOC后實際上所調用的malloc原型,malloc已經成了一個宏定義
#define
malloc(s)
_malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)
當然,我們一般調用_CrtDumpMemoryLeaks的時候都是在程序的結尾處,如果我們的程序有多個出口,我們值需要在程序開始處調用_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF
) 就可以了
有時候我們需要檢測某一段代碼是否有內存泄露,crtdbg庫也可以幫我們做到
_CrtMemState s1;
_CrtMemState s2;
_CrtMemCheckpoint(&s1);
char *p2 = (char *)malloc(400);
_CrtMemCheckpoint(&s2);
_CrtMemState s3;
if (_CrtMemDifference(&s3,&s1,&s2))
{
_CrtMemDumpStatistics(&s3);
}
這樣,我們在輸出窗口將會看到s1和s2之間的內存使用信息:
0 bytes in 0 Free Blocks.
400 bytes in 1 Normal Blocks.
0 bytes in 0 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 0 bytes.
Total allocations: 400 bytes.
crtdbg庫也有缺點,當你使用一個別人提供的lib或者dll庫的時候,你調用這個函數,這個函數分配了內存,需要你去調用另外一個函數才能把內存釋放掉,但是你不知道這個函數需要調用另外一個函數才能釋放掉內存,這個是無法通過crtdbg庫檢測出來的,這個函數包括c++的new函數,所以這個庫實際上不適用C++
利用share_ptr來管理內存
如果有使用過boost庫的應該知道,boost里面有一個shart_ptr被譽為神器,因為他可以幫我們自動管理內存,具體用法很簡單:
boost::shared_ptr < connection > p ( new connection());
這樣的話我們不需要去delete內存,shartd_ptr會在我們不需要這快內存的時候幫我們delete掉,shartd_ptr內部是使用引用計數以及C++的RAII,有別的對象引用該指針的時候引用技術就+1,shartd_ptr析構函數調用的時候引用計數就-1,當為0的時候就delete掉該指針,所以我們并不需要調用delete來釋放資源,share_ptr會幫我們管理
shared_ptr雖然看起來很好用,但是當程序一旦復雜起來,shared_ptr依然也會變復雜(shared_ptr四宗罪),當然boost本身就比較復雜,這個也是我比較不喜歡boost的一個原因
將資源集中管理
這個也是我比較經常使用的方法,特別是在大程序的使用,配合單件模式,將資源在整個程序或者模塊中集中管理,這樣在程序結束的時候只要我們在析構函數里面有清理這些資源,我們就可以避免內存泄露,對于數據的一些寫操作全部在這個類中統一操作,如果要暴露內部的數據,只對外提供const數據(可以通過強轉去掉const屬性)
當然這個方法并不適用于所有場景,比如我們需要提供庫給別人,我們沒辦法預測到客戶需要什么操作,所以這個方法只適用內部團隊開發
總之內存管理據我所知到現在還是沒有什么好的解決方法,特別是當代碼一旦膨脹的時候,到現在好像java,python,erlang都有內存泄露的問題,我們只能在平常開發中多注意了
參考資料:
陳碩的博客(有一些shared_ptr的資料,也可以從這里看出shared_ptr使用起來沒那么簡單)
shared_ptr四宗罪
MSDN crtdbg庫
本文相關鏈接:http://blog.csdn.net/na_he/article/details/7429171
http://www.cnblogs.com/linyilong3/archive/2013/03/23/2977247.html
posted on 2013-05-02 18:29
王海光 閱讀(7167)
評論(0) 編輯 收藏 引用 所屬分類:
C++