1. 程式人生 > >記憶體洩漏及常見的解決辦法

記憶體洩漏及常見的解決辦法

之所以撰寫這篇文章是因為前段時間花費了很大的精力在已經成熟的程式碼上再去處理memory leak問題。寫此的目的是希望我們應該養成良好的編碼習慣,儘可能的避免這樣的問題,因為當你對著一大片的程式碼再去處理此類的問題,此時無疑增加了解決的成本和難度。準確的說屬於補救措施了。

1. 什麼是記憶體洩漏(memory leak)?

指由於疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況。記憶體洩漏並非指記憶體在物理上的消失,而是應用程式分配某段記憶體後,由於設計錯誤,失去了對該段記憶體的控制,因而造成了記憶體的浪費。 

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:對於系統資源使用之前要仔細看使用方法,防止錯誤使用或忘記釋放掉系統資源。
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++複雜老練的物件管理設施來管理資源.

 
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。這也是大家編碼過程中常常使用的方法。
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下如何防止記憶體洩漏:

在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,就可以輸出記憶體洩漏的資訊。

我們在main.cpp這個檔案中的第8行申請了記憶體,但是沒有進行釋放。

那麼編譯器怎麼知道我們有記憶體洩漏呢??就是利用巨集定義把我們呼叫的malloc替換成crtdbg庫裡面的_malloc_dbg會先記錄下我們申請記憶體的行數以及大小(記得編譯器有內建的巨集定義__LINE__和__FILE__不?)把這些資訊放到一個list(只是舉例,使用list儲存這些資訊,一旦程式大了會很慢)裡面,當我們free記憶體的時候,把這塊記憶體的資訊從list裡面刪除掉,我們呼叫_CrtDumpMemoryLeaks()就是把這個list資訊依次打印出來而已。

當然,我們一般呼叫_CrtDumpMemoryLeaks的時候都是在程式結尾處,如果我們的程式有多個出口,我們只需要在程式開始處呼叫_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)就可以。

_CrtMemState s1;
_CrtMemState s2;  
_CrtMemCheckpoint(&s1); 
char *p2 = (char *)malloc(400);
_CrtMemCheckpoint(&s2);
_CrtMemState s3;
if (_CrtMemDifference(&s3,&s1,&s2))
{
     _CrtMemDumpStatistics(&s3);
}
crtdbg庫也有缺點,當你使用別人使用的lib或dll庫的時候,你呼叫這個函式,這個函式分配了記憶體,需要你去呼叫另外一個函式才能把記憶體釋放掉,但是你不知道這個函式需要呼叫另外一個函式才能釋放掉記憶體,這個是無法通過crtdbg庫檢測出來的,這個函式包括C++的new函式,所以這個庫實際上不適用C++

利用share_ptr來管理記憶體
如果有使用過boost庫的應該知道,boost裡面有一個share_ptr被譽為神器。因為他可以幫我們自動管理記憶體,具體用法很簡單:

 
 boost::shared_ptr < connection > p ( new connection());
這樣的話我們不需要去delete記憶體,share_ptr會在我們不需要記憶體的時候幫我們delete掉,shartd_ptr內部是使用引用計數以及C++的RAII,有別的物件引用該指標的時候引用計數就+1,shartd_ptr解構函式呼叫的時候引用計數就減一,當為0的時候就delete掉該指標,所以我們並不需要delete來釋放資源,share_ptr會幫我們整理。
將資源集中管理
這個也是我比較經常使用的方法,特別是在大程式的使用,配合單件模式,將資源在整個程式或模組中集中管理,這樣在程式結束的時候只要 我們在解構函式裡面有清理這些資源,我們就可以避免記憶體洩露,對於資料的一些寫操作全部在這個類中統一操作,如果要暴露內部的資料,只對外提供const資料
http://www.cppblog.com/wanghaiguang/archive/2013/05/02/199909.aspx