之所以撰寫這篇文章是由於前段時間花費了非常大的精力在已經成熟的代碼上再去處理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, &ps);
假設使用完畢時候忘記釋放字體,就造成了資源泄漏。
對於基於引用計數的系統對象尤其要注意,由於僅僅有其引用計數為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)資源獲取即初始化,它是一項非常easy的技術,利用C++對象生命周期的概念來控制程序的資源,比如內存,文件句柄,網絡連接以及審計追蹤(audit trail)等.RAII的基本技術原理非常easy.若希望保持對某個重要資源的跟蹤,那么創建一個對象,並將資源的生命周期和對象的生命周期相關聯.如此一來,就能夠利用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 循環語句中,循環次數非常easy搞錯,導致數組操作越界。
使用free 或delete 釋放了內存后,沒有將指針設置為NULL。導致產生“野指針”。
程序中的對象調用關系過於復雜,實在難以搞清楚某個對象到底是否已經釋放了內存,此時應該又一次設計數據結構,從根本上解決對象管理的混亂局面。(這點但是深有感受,呵呵)
不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。