淺談new-delete和malloc-free的用法和區別
0 目錄
1 new和delete的用法
如下幾行程式碼:
int *pi = new int;
int *pi = new int();
int *pi = new int(1024);
- 第一行程式碼就是動態生成了儲存一個整型物件的空間,並把該空間的地址賦給整形指標
pi
; - 第二行和第一行一樣,只是將分配的儲存空間的內容初始化為
0
; - 第三行初始化為
1024
;
動態建立的物件必須在用完之後,必須再釋放掉,避免造成記憶體洩漏。使用delete
函式完成。new
和delete
是一對。
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所指向的陣列
到這兒,不免會有疑問,new
和delete
到底是什麼?操作符還是函式?
2 new和delete深入理解
2.1 概念理解
首先,operator new
與 new operator
是不同的概念。operator new
和 operator delete
其實是C++ 語言標準庫的庫函式, 原型分別如下:
void *operator new(size_t); // 分配一個物件 void *operator delete(void *); // 釋放一個物件 void *operator new[](size_t); // 分配一個數組 void *operator delete[](void *); // 釋放一個數組
前面兩個均是C++標準庫函式,你可能會覺得這是函式嗎?請不要懷疑,這就是函式!這兩個函式和malloc
和 free
函式有點像了,都是用來申請和釋放記憶體的,並且 operator new
申請記憶體之後不對記憶體進行初始化,直接返回申請記憶體的指標。
2.2 new和delete背後機制
知道上面兩個函式之後,我們用一個例項來解釋 new
和 delete
背後的機制。 我們不用簡單的 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 背後完成的工作:
簡單總結一下:
-
首先, 需要呼叫上面提到的
operator new
標準庫函式,傳入的引數為class A
的大小,這裡為 8 個位元組,至於為什麼是 8 個位元組,你可以看看《深入C++物件模型》一書,這裡不做多解釋。這樣函式返回的是分配記憶體的起始地址,這裡假設是0x007da290
。 -
上面分配的記憶體是未初始化的, 也是未型別化的, 第二步就在這一塊原始的記憶體上對類物件進行初始化, 呼叫的是相應的建構函式, 這裡是呼叫 A:A(10); 這個函式,從圖中也可以看到對這塊申請的記憶體進行了初始化,var=10,
file
指向開啟的檔案。 -
最後一步就是返回新分配並構造好的物件的指標,這裡
pA
就指向0x007da290
這塊記憶體,pA
的型別為類 A 物件的指標。
所有這三步,你都可以通過反彙編找到相應的彙編程式碼,在這裡我就不列出了。
好了,那麼 delete
都幹了什麼呢?還是接著上面的例子,如果這時想釋放掉申請的類的物件怎麼辦?當然我們可以使用下面的語句來完成:
delete pA;
delete 所做的事情如下圖所示:
delete 就做了兩件事情:
-
呼叫 pA 指向物件的解構函式,對開啟的檔案進行關閉。
-
通過上面提到的標準庫函式
operator delete
來釋放該物件的記憶體,傳入函式的引數為 pA 的值,也就是0x007d290
。
好了,解釋完了 new 和 delete 背後所做的事情了,是不是覺得也很簡單?不就多了一個建構函式和解構函式的呼叫嘛。
2.3 如何申請和釋放一個數組
我們經常要用到動態分配一個數組,也許是這樣的:
string *psa = new string[10]; // 10個字串型別的元素的陣列
int *pia = new int[10]; // 10個整形元素的陣列,未初始化
上面在申請一個數組時都用到了 new[]
這個表示式來完成,按照我們上面講到的 new
和 delete
知識,第一個陣列是 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];
時需要做的事情如下:
從這個圖中我們可以看到申請時在陣列物件的上面還多分配了 4 個位元組用來儲存陣列的大小, 但是最終返回的是物件陣列的指標, 而不是所有分配空間的起始地址。
這樣的話,釋放就很簡單了:
delete [] pAa;
這裡要注意的兩點是:
- 呼叫解構函式的次數是從陣列物件指標前面的 4 個位元組中取出;
- 傳入
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; 做了兩件事:
- 呼叫一次 pAa 指向的物件的解構函式;
- 呼叫
operator delete(pAa);
釋放記憶體。
顯然,這裡只對陣列的第一個類物件呼叫了解構函式,後面的兩個物件均沒呼叫解構函式,如果類物件中申請了大量的記憶體需要在解構函式中釋放,而你卻在銷燬陣列物件時少呼叫了解構函式,這會造成記憶體洩漏。
上面的問題你如果說沒關係的話,那麼第二點就是致命的了!直接釋放 pAa 指向的記憶體空間, 這個總是會造成嚴重的段錯誤,程式必然會奔潰!因為分配的空間的起始地址是 pAa 指向的地方減去 4 個位元組的地方。你應該傳入引數設為那個地址!
同理,你可以分析如果使用 new
來分配,用 delete []
來釋放會出現什麼問題?是不是總會導致程式錯誤?
總的來說,記住一點即可:new/delete
、new[]/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);
malloc
和calloc
間的主要區別在於, 後者在返回指向記憶體的指標之前把它初始化為0。 另一個區別是calloc
的引數包括所需的元素的數量和每個元素的位元組數。
realloc
函式用於修改一個原先已經分配的記憶體塊的大小。可以使一塊記憶體擴大或縮小,如果擴大記憶體,則原來的記憶體塊保持不變,在記憶體尾部增加新的記憶體塊,切不進行初始化。如果縮小記憶體,則原來記憶體塊從尾部進行刪減。如果原先的記憶體塊無法擴充,則新開闢一塊記憶體,並複製原先的記憶體的內容,原先記憶體塊失效無法再進行訪問。
4 new和malloc的區別
-
屬性
new/delete
是C++關鍵字,需要編譯器支援。malloc/free
是庫函式,需要標頭檔案支援c。 -
引數
使用
new
操作符申請記憶體分配時無須指定記憶體塊的大小,編譯器會根據型別資訊自行計算。而malloc
則需要顯式地指出記憶體的大小。 -
返回型別
new
申請記憶體分配成功時,返回的是物件型別的指標,型別嚴格與物件匹配,無須進行型別轉換,故new
是符合型別安全性的操作符。 而malloc
記憶體分配成功則是返回void *
,需要通過強制型別轉換將void*
指標轉換成我們需要的型別。 -
分配失敗
new
記憶體分配失敗時,會丟擲bac_alloc
異常。malloc
分配記憶體失敗時返回NULL
。 -
自定義型別
new
會先呼叫operator new
函式,申請足夠的記憶體(通常底層使用malloc
實現)。 然後呼叫型別的建構函式, 初始化成員變數, 最後返回自定義型別指標。delete
先呼叫解構函式,然後呼叫operator delete
函式釋放記憶體(通常底層使用free
實現)。malloc/free
是庫函式,只能動態地申請和釋放記憶體,無法強制要求其做自定義型別物件構造和析構工作。 -
過載
C++允許過載
new/delete
操作符,特別的,placement new
的就不需要為物件分配記憶體,而是指定了一個地址作為記憶體起始區域,new在這段記憶體上為物件呼叫建構函式完成初始化工作,並返回此地址。而malloc
不允許過載。 -
記憶體區域
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++的自由儲存區可以包括堆區,也可以包括其他區域。