1. 程式人生 > >淺談new-delete和malloc-free的用法和區別

淺談new-delete和malloc-free的用法和區別

0 目錄

1 new和delete的用法

如下幾行程式碼:

int *pi = new int;
int *pi = new int();
int *pi = new int(1024);
  1. 第一行程式碼就是動態生成了儲存一個整型物件的空間,並把該空間的地址賦給整形指標pi
  2. 第二行和第一行一樣,只是將分配的儲存空間的內容初始化為0
  3. 第三行初始化為1024

動態建立的物件必須在用完之後,必須再釋放掉,避免造成記憶體洩漏。使用delete函式完成。newdelete是一對。

delete pi;

此時,雖然pi所指向的地址裡的內容雖然被釋放,但是其所存放的地址還在。為了保險起見,可以在釋放物件後,立即將其指標設定為NULL

,清楚地表明不再指向任何物件。

pi = NULL;

C++允許動態建立const物件:

const int  *pi = new const int(1024);

動態建立的const物件必須進行初始化,並且初始化後的值不能再被改變。

當建立一個動態陣列物件和進行記憶體釋放時,執行以下語句:

int *pi = new int[];        // 指標pi所指向的陣列未初始化
int *pi = new int[n];       // 指標pi指向長度為n的陣列,未初始化
int *pi = new int[]();      // 指標pi所指向的地址初始化為0
delete[] pi;                // 回收pi所指向的陣列

到這兒,不免會有疑問,newdelete到底是什麼?操作符還是函式?

回到頂部

2 new和delete深入理解

2.1 概念理解

首先,operator newnew operator是不同的概念。operator newoperator delete其實是C++ 語言標準庫的庫函式, 原型分別如下:

void *operator new(size_t);         // 分配一個物件
void *operator delete(void *);      // 釋放一個物件

void *operator new[](size_t);       // 分配一個數組
void *operator delete[](void *);    // 釋放一個數組

前面兩個均是C++標準庫函式,你可能會覺得這是函式嗎?請不要懷疑,這就是函式!這兩個函式和mallocfree 函式有點像了,都是用來申請和釋放記憶體的,並且 operator new申請記憶體之後不對記憶體進行初始化,直接返回申請記憶體的指標。

2.2 new和delete背後機制

知道上面兩個函式之後,我們用一個例項來解釋 newdelete 背後的機制。 我們不用簡單的 C++ 內建型別來舉例, 使用複雜一點的類型別,定義一個類 A:

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 背後完成的工作:

new_delete_1

簡單總結一下:

  1. 首先, 需要呼叫上面提到的 operator new 標準庫函式,傳入的引數為class A 的大小,這裡為 8 個位元組,至於為什麼是 8 個位元組,你可以看看《深入C++物件模型》一書,這裡不做多解釋。這樣函式返回的是分配記憶體的起始地址,這裡假設是 0x007da290

  2. 上面分配的記憶體是未初始化的, 也是未型別化的, 第二步就在這一塊原始的記憶體上對類物件進行初始化, 呼叫的是相應的建構函式, 這裡是呼叫 A:A(10); 這個函式,從圖中也可以看到對這塊申請的記憶體進行了初始化,var=10, file 指向開啟的檔案。

  3. 最後一步就是返回新分配並構造好的物件的指標,這裡 pA 就指向 0x007da290 這塊記憶體,pA 的型別為類 A 物件的指標。

所有這三步,你都可以通過反彙編找到相應的彙編程式碼,在這裡我就不列出了。

好了,那麼 delete 都幹了什麼呢?還是接著上面的例子,如果這時想釋放掉申請的類的物件怎麼辦?當然我們可以使用下面的語句來完成:

delete pA;

delete 所做的事情如下圖所示:

new_delete_2

delete 就做了兩件事情:

  1. 呼叫 pA 指向物件的解構函式,對開啟的檔案進行關閉。

  2. 通過上面提到的標準庫函式 operator delete 來釋放該物件的記憶體,傳入函式的引數為 pA 的值,也就是 0x007d290

好了,解釋完了 new 和 delete 背後所做的事情了,是不是覺得也很簡單?不就多了一個建構函式和解構函式的呼叫嘛。

2.3 如何申請和釋放一個數組

我們經常要用到動態分配一個數組,也許是這樣的:

string *psa = new string[10];       // 10個字串型別的元素的陣列
int *pia = new int[10];             // 10個整形元素的陣列,未初始化

上面在申請一個數組時都用到了 new[] 這個表示式來完成,按照我們上面講到的 newdelete 知識,第一個陣列是 string 型別,分配了儲存物件的記憶體空間之後,將呼叫 string 型別的預設建構函式依次初始化陣列中每個元素; 第二個是申請具有內建型別的陣列, 分配了儲存 10 個 int 物件的記憶體空間,但並沒有初始化。

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

delete [] psa;
delete [] pia;

都用到 delete[] 表示式,注意這地方的 [] 一般情況下不能漏掉!我們也可以想象這兩個語句分別幹了什麼:第一個對 10 個 string 物件分別呼叫解構函式,然後再釋放掉為物件分配的所有記憶體空間;第二個因為是內建型別不存在解構函式,直接釋放為 10 個 int 型分配的所有記憶體空間。

這裡對於第一種情況就有一個問題了:我們如何知道 psa 指向物件的陣列的大小?怎麼知道呼叫幾次解構函式?

這個問題直接導致我們需要在 new [] 一個物件陣列時,需要儲存陣列的維度,C++ 的做法是在分配陣列空間時多分配了 4 個位元組的大小, 專門儲存陣列的大小, 在 delete [] 時就可以取出這個儲存的數,就知道了需要呼叫解構函式多少次了。

還是用圖來說明比較清楚,我們定義了一個類 A,但不具體描述類的內容,這個類中有顯示的建構函式、解構函式等。那麼 當我們呼叫

class A *pAa = new A[3];

時需要做的事情如下:

new_delete_3

從這個圖中我們可以看到申請時在陣列物件的上面還多分配了 4 個位元組用來儲存陣列的大小, 但是最終返回的是物件陣列的指標, 而不是所有分配空間的起始地址。

這樣的話,釋放就很簡單了:

delete [] pAa;

new_delete_4

這裡要注意的兩點是:

  1. 呼叫解構函式的次數是從陣列物件指標前面的 4 個位元組中取出;
  2. 傳入 operator delete[] 函式的引數不是陣列物件的指標 pAa,而是 pAa 的值減 4。

2.4 為什麼 new/delete 、new []/delete[] 要配對使用?

其實說了這麼多,還沒到我寫這篇文章的最原始意圖。從上面解釋的你應該懂了new/delete、new[]/delete[] 的工作原理了, 因為它們之間有差別, 所以需要配對使用。但偏偏問題不是這麼簡單,這也是我遇到的問題,如下這段程式碼:

int *pia = new int[10];
delete []pia;

這肯定是沒問題的,但如果把 delete []pia; 換成 delete pia; 的話,會出問題嗎?

這就涉及到上面一節沒提到的問題了。上面我提到了在 new [] 時多分配 4 個位元組的緣由, 因為析構時需要知道陣列的大小 ,但如果不呼叫解構函式呢(如內建型別,這裡的 int 陣列)?我們在 new [] 時就沒必要多分配那 4 個位元組, delete [] 時直接到第二步釋放為 int 陣列分配的空間。如果這裡使用 delete pia;那麼將會呼叫 operator delete 函式, 傳入的引數是分配給陣列的起始地址,所做的事情就是釋放掉這塊記憶體空間。不存在問題的。

這裡說的使用 new []delete 來釋放物件的提前是:物件的型別是內建型別或者是無自定義的解構函式的類型別!

我們看看如果是帶有自定義解構函式的類型別,用 new [] 來建立類物件陣列,而用 delete 來釋放會發生什麼?用上面的例子來說明:

class A *pAa = new class A[3];
delete pAa;

那麼 delete pAa; 做了兩件事:

  1. 呼叫一次 pAa 指向的物件的解構函式;
  2. 呼叫 operator delete(pAa); 釋放記憶體。

顯然,這裡只對陣列的第一個類物件呼叫了解構函式,後面的兩個物件均沒呼叫解構函式,如果類物件中申請了大量的記憶體需要在解構函式中釋放,而你卻在銷燬陣列物件時少呼叫了解構函式,這會造成記憶體洩漏。

上面的問題你如果說沒關係的話,那麼第二點就是致命的了!直接釋放 pAa 指向的記憶體空間, 這個總是會造成嚴重的段錯誤,程式必然會奔潰!因為分配的空間的起始地址是 pAa 指向的地方減去 4 個位元組的地方。你應該傳入引數設為那個地址!

同理,你可以分析如果使用 new 來分配,用 delete [] 來釋放會出現什麼問題?是不是總會導致程式錯誤?

總的來說,記住一點即可:new/deletenew[]/delete[] 要配套使用總是沒錯的!

回到頂部

3 malloc和free的用法

兩個函式的原型如下,他們都在標頭檔案stdlib.h中宣告。

void *malloc(size_t size);
void free(void *pointer);

示例程式碼如下:

int *p = (int *)malloc(100);            //指向整型的指標p指向一個大小為100位元組的記憶體的地址
int *p = (int *)malloc(25*sizeof(int)); //指向整型的指標p指向一個25個int整型空間的地址

因為malloc()函式的返回值型別為void *,所以需要在函式前面進行相應的強制型別轉換。當int佔4個位元組記憶體時,上述的兩個語句程式碼獲得的記憶體空間大小是相同的。分配記憶體後必須驗證記憶體是否分配成功,完成後用free()釋放記憶體,完整語句如下。

int *p=(int *)malloc(int);
if(pi==NULL)
    printf("Out of memory!\n");
free (p);

另外還有兩個分配記憶體的函式:calloc和realloc,他們的原型如下:

void *calloc(size_t num_elements,size_t element_size);
void realloc(void *tr , size_t new_size);

malloccalloc間的主要區別在於, 後者在返回指向記憶體的指標之前把它初始化為0。 另一個區別是calloc的引數包括所需的元素的數量和每個元素的位元組數。

realloc函式用於修改一個原先已經分配的記憶體塊的大小。可以使一塊記憶體擴大或縮小,如果擴大記憶體,則原來的記憶體塊保持不變,在記憶體尾部增加新的記憶體塊,切不進行初始化。如果縮小記憶體,則原來記憶體塊從尾部進行刪減。如果原先的記憶體塊無法擴充,則新開闢一塊記憶體,並複製原先的記憶體的內容,原先記憶體塊失效無法再進行訪問。

回到頂部

4 new和malloc的區別

  1. 屬性

    new/delete是C++關鍵字,需要編譯器支援。malloc/free是庫函式,需要標頭檔案支援c。

  2. 引數

    使用new操作符申請記憶體分配時無須指定記憶體塊的大小,編譯器會根據型別資訊自行計算。而malloc則需要顯式地指出記憶體的大小。

  3. 返回型別

    new申請記憶體分配成功時,返回的是物件型別的指標,型別嚴格與物件匹配,無須進行型別轉換,故new是符合型別安全性的操作符。 而malloc記憶體分配成功則是返回void * ,需要通過強制型別轉換將void*指標轉換成我們需要的型別。

  4. 分配失敗

    new記憶體分配失敗時,會丟擲bac_alloc異常。malloc分配記憶體失敗時返回NULL

  5. 自定義型別

    new會先呼叫operator new函式,申請足夠的記憶體(通常底層使用malloc實現)。 然後呼叫型別的建構函式, 初始化成員變數, 最後返回自定義型別指標。delete先呼叫解構函式,然後呼叫operator delete函式釋放記憶體(通常底層使用free實現)。

    malloc/free是庫函式,只能動態地申請和釋放記憶體,無法強制要求其做自定義型別物件構造和析構工作。

  6. 過載

    C++允許過載new/delete操作符,特別的,placement new的就不需要為物件分配記憶體,而是指定了一個地址作為記憶體起始區域,new在這段記憶體上為物件呼叫建構函式完成初始化工作,並返回此地址。而malloc不允許過載。

  7. 記憶體區域

    new操作符從自由儲存區(free store)上為物件動態分配記憶體空間,而malloc函式從堆上動態分配記憶體。自由儲存區是C++基於new操作符的一個抽象概念,凡是通過new操作符進行記憶體申請,該記憶體即為自由儲存區。而堆是作業系統中的術語,是作業系統所維護的一塊特殊記憶體,用於程式的記憶體動態分配,C語言使用malloc從堆上分配記憶體, 使用free釋放已分配的對應記憶體。自由儲存區不等於堆,如上所述,佈局new就可以不位於堆中。

PS:

在C++中,記憶體區分為5個區,分別是堆、棧、自由儲存區、全域性/靜態儲存區、常量儲存區;

在C中,C記憶體區分為堆、棧、全域性/靜態儲存區、常量儲存區;

new預設的實現方式本質上是通過malloc的,這個時候,C++的自由儲存區的概念和C的堆的概念是沒有區別的, 但是, 如果我們通過過載operator new的方式把記憶體分配在一些全域性變數上,那麼這些記憶體就不屬於堆區了,而是在data segment。也就是說,C++的自由儲存區可以包括堆區,也可以包括其他區域。

回到頂部