天天看點

C++記憶體管理分析

malloc/calloc/realloc的差別?

1. malloc

函數原型:

void *malloc(size_t size);

函數功能:

malloc()在記憶體的動态存儲區中配置設定一塊長度為size位元組的連續區域。參數size為需要的記憶體空間的長度,傳回該區域的位址。

差別:

malloc不能初始化所配置設定的記憶體空間,需要用memset,而函數calloc能初始化。如果這部分記憶體曾經被配置設定過,則其中可能遺留各種各樣的資料。

2. calloc

函數原型:

void *calloc(size_t nmemb, size_t size);

函數功能:

calloc()與malloc()相似,參數size為申請位址的機關元素長度,nmemb為參數個數。

差別:

calloc會将所配置設定的空間中的每一位都初始化為零。

3. realloc

函數原型:

void *realloc(void *ptr, size_t size);

函數功能:

realloc()是給一個已經配置設定了位址的指針重新配置設定空間,參數ptr為原有的空間位址,newsize是重新申請的位址空間。

差別:

realloc可以對給定的指針所指向的空間進行擴大或縮小,原有的記憶體中的内容将保持不變。realloc并不保持調整後的記憶體空間和原來的記憶體空間保持同一記憶體位址,傳回的指針很可能指向新的位址。

記憶體洩漏?如何檢測記憶體洩漏?

C++中的記憶體洩露一般指堆中的記憶體洩露。堆記憶體是我們手動malloc/realloc/new申請的,程式不會自動回收,需要調用free或delete手動釋放,否則就會造成記憶體洩露。

記憶體洩露的關鍵就是記錄配置設定的記憶體和釋放記憶體的操作,看看能不能比對。跟蹤每一塊記憶體的生命周期。

例如:每當申請一塊記憶體後,把指向它的指針加入到List中,當釋放時,再把對應的指針從List中删除,到程式最後檢查List就可以知道有沒有記憶體洩露了。Window平台下的Visual Studio調試器和C運作時(CRT)就是用這個原理來檢測記憶體洩露。

在VS中使用時,需加上

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
...
_CrtDumpMemoryLeaks();      

crtdbg.h的作用是将malloc和free函數映射到它們的調試版本_malloc_dbg和_free_dbg,這兩個函數将跟蹤記憶體配置設定和釋放(在Debug版本中有效)。

_CrtDumpMemoryLeaks函數将顯示目前記憶體洩露,也就是說程式運作到此行代碼時的記憶體洩露,所有未銷毀的對象都會報出記憶體洩露,是以要讓這個函數盡量放到最後。

舉例如下:

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#include <iostream>
using namespace std;

int main(int argc,char** argv)
{
    char *str1 = NULL;
    char *str2 = NULL;
    str1=new char[100];
    str2=new char[50];

    delete str1;

    _CrtDumpMemoryLeaks();

    return 0;

}      

上述代碼中,我們隻釋放了一塊記憶體,運作調試,會在output視窗輸出:

C++記憶體管理分析

可以看到檢測到了記憶體洩露。

但是并沒有檢測到洩露記憶體申請的位置,我們已經加了宏定義#define _CRTDBG_MAP_ALLOC。原因是申請記憶體用的是new,而剛剛包含頭檔案和加宏定義是重載了malloc函數,并沒有重載new操作符,是以要自己定義重載new操作符才能檢測到洩露記憶體的申請位置。修改如下:

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG    //重載new
#define new  new(_NORMAL_BLOCK, __FILE__, __LINE__)  
#endif
#include <iostream>
using namespace std;

int main(int argc,char** argv)
{
    char *str1 = NULL;
    char *str2 = NULL;
    str1=(char*)malloc(100);
    str2=new char[50];

    _CrtDumpMemoryLeaks();

    return 0;
}      

我們再看調試結果:

C++記憶體管理分析

源.cpp()括号裡面的數字就是洩露記憶體的起始位置。

後面的{144} normal block at 0x0361B1C0, 50 bytes long代表什麼呢?

大括号{}裡面的數字表示第幾次申請記憶體操作;0x0361B1C0表示洩露記憶體的起始位址,CD CD表示洩露記憶體的内容。

調用long _CrtSetBreakAlloc(long nAllocID)可以再第nAllocID次申請記憶體時中斷,在中斷時擷取的資訊比在程式終止時擷取的資訊要多,你可以調試,檢視變量狀态,對函數調用調試分析,解決記憶體洩露。

block分為3中類型,此處為normal,表示普通,此外還有client表示用戶端(專門用于MFC),CRT表示運作時(有CRT庫來管理,一般不會洩露),free表示已經釋放掉的塊,igore表示要忽略的塊。

在上面程式中,調用_CrtDumpMemoryLeaks()來檢測記憶體洩露,如果程式可能在多個地方終止,必須在多個地方調用這個函數,這樣比較麻煩,可以在程式起始位置調用_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF| _CRTDBG_LEAK_CHECK_DF),這樣無論程式何時終止,都會在終止前調用_CrtDumpMemoryLeaks()。

除此之外,還可以在某時刻設定檢查點,擷取當時記憶體狀态的快照。比較不同時刻記憶體狀态的差異。

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG //重載new
#define new  new(_NORMAL_BLOCK, __FILE__, __LINE__)  
#endif
#include <iostream>
using namespace std;

int main(int argc,char** argv)
{
    _CrtMemState s1, s2, s3;
    char *str1 = NULL;
    char *str2 = NULL;
    str1=(char*)malloc(100);
    _CrtMemCheckpoint( &s1 );//記錄記憶體快照
    _CrtMemDumpStatistics( &s1 );//輸出
    str2=new char[50];
    _CrtMemCheckpoint( &s2 );
    _CrtMemDumpStatistics( &s2 );

    if ( _CrtMemDifference( &s3, &s1, &s2) )//比較s1和s2,把比較結果輸出到s3
    _CrtMemDumpStatistics( &s3 );// dump 差異結果

    return 0;
}      

調試結果:

C++記憶體管理分析

new/delete操作符

我們都知道,new/delete是對于單個對象空間,new[]/delete[]是對于連續空間,且必須配對使用。

先看幾個C++語言标準庫的庫函數:

void *operator new(size_t);     //allocate an object
void *operator delete(void *);    //free an object

void *operator new[](size_t);     //allocate an array
void *operator delete[](void *);    //free an array      

看一個例子:

class A
{
public:
    A(int v) : var(v)
    {
        fopen_s(&file, "test", "r");
    }
    ~A()
    {
        fclose(file);
    }

private:
    int var;
    FILE *file;
};      

上面類A中有兩個私有成員,有一個構造函數和一個析構函數,構造函數中初始化私有變量 var 以及打開一個檔案,析構函數關閉打開的檔案。

我們使用class A *pA = new A(10);來建立一個類的對象,傳回其指針 pA。

new 背後完成的工作:
  1. 首先需要調用上面提到的 operator new 标準庫函數,傳入的參數為 class A 的大小,這裡為 8 個位元組。
  2. 上面配置設定的記憶體是未初始化的,也是未類型化的,第二步就在這一塊原始的記憶體上對類對象進行初始化,調用的是相應的構造函數,這裡是調用 A::A(10); 這個函數,var=10, file 指向打開的檔案。
  3. 最後一步就是傳回新配置設定并構造好的對象的指針,這裡 pA 就指向這塊記憶體,pA 的類型為類 A 對象的指針。

那麼 delete 都幹了什麼呢?還是接着上面的例子,如果這時想釋放掉申請的類的對象怎麼辦?我們使用下面的語句來完成:

delete pA;

delete 所做的事情:
  1. 調用 pA 指向對象的析構函數,對打開的檔案進行關閉。
  2. 通過上面提到的标準庫函數 operator delete 來釋放該對象的記憶體,傳入函數的參數為 pA 的值。
如何申請和釋放一個數組?

我們經常要申請一個數組,如下:

string *psa = new string[10];      //array of 10 empty strings
int *pia = new int[10];           //array of 10 uninitialized ints      

上面在申請一個數組時都用到了new[]這個表達式來完成:

第一個數組是 string 類型,配置設定了儲存對象的記憶體空間之後,将調用 string 類型的預設構造函數依次初始化數組中每個元素;

第二個是申請具有内置類型的數組,配置設定了存儲 10 個 int 對象的記憶體空間,但并沒有初始化。

如果我們想釋放空間了,可以用下面兩條語句:

delete []psa;
delete []pia;      

我們需要注意:

  1. 調用析構函數的次數是從數組對象指針前面的 4 個位元組中取出;
  2. 傳入 operator delete[] 函數的參數不是數組對象的指針p,而是 p 的值減 4。
malloc/free&&new/delete的差別
  1. malloc/free是C/C++語言的标準庫函數,new/delete是C++的運算符
  2. new能夠自動配置設定空間大小
  3. 對于使用者自定義的對象而言,用maloc/free無法滿足動态管理對象的要求。對象在建立的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由于malloc/free是庫函數而不是運算符,不在編譯器控制權限之内,不能夠把執行構造函數和析構函數的任務強加于malloc/free。

    是以C++需要一個能對對象完成動态記憶體配置設定和初始化工作的運算符new,以及一個能對對象完成清理與釋放記憶體工作的運算符delete