在 C++ 中,你也許經常使用 new 和 delete 來動态申請和釋放記憶體,但你可曾想過以下問題呢?
- new 和 delete 是函數嗎?
- new [] 和 delete [] 又是什麼?什麼時候用它們?
- 你知道 operator new 和 operator delete 嗎?
- 為什麼 new [] 出來的數組有時可以用 delete 釋放有時又不行?
- …
如果你對這些問題都有疑問的話,不妨看看我這篇文章。
new 和 delete 到底是什麼?
如果找工作的同學看一些面試的書,我相信都會遇到這樣的題:sizeof 不是函數,然後舉出一堆的理由來證明 sizeof 不是函數。在這裡,和 sizeof 類似,new 和 delete 也不是函數,它們都是 C++ 定義的關鍵字,通過特定的文法可以組成表達式。和 sizeof 不同的是,sizeof 在編譯時候就可以确定其傳回值,new 和 delete 背後的機制則比較複雜。
繼續往下之前,請你想想你認為 new 應該要做些什麼?也許你第一反應是,new 不就和 C 語言中的 malloc 函數一樣嘛,就用來動态申請空間的。你答對了一半,看看下面語句:
你就可以看出 new 和 malloc 還是有點不同的,malloc 申請完空間之後不會對記憶體進行必要的初始化,而 new 可以。是以 new expression 背後要做的事情不是你想象的那麼簡單。在我用執行個體來解釋 new 背後的機制之前,你需要知道
operator new
和
operator delete
是什麼玩意。
operator new 和 operator 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
後面兩個你可以先不看,後面再介紹。前面兩個均是 C++ 标準庫函數,你可能會覺得這是函數嗎?請不要懷疑,這就是函數!C++ Primer 一書上說這不是重載 new 和 delete 表達式(如
operator=
就是重載 = 操作符),因為 new 和 delete 是不允許重載的。但我還沒搞清楚為什麼要用 operator new 和 operator delete 來命名,比較費解。我們隻要知道它們的意思就可以了,這兩個函數和 C 語言中的 malloc 和 free 函數有點像了,都是用來申請和釋放記憶體的,并且 operator new 申請記憶體之後不對記憶體進行初始化,直接傳回申請記憶體的指針。
我們可以直接在我們的程式中使用這幾個函數。
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 以及打開一個檔案,析構函數關閉打開的檔案。
我們使用
來建立一個類的對象,傳回其指針 pA。如下圖所示 new 背後完成的工作:
簡單總結一下:
- 首先需要調用上面提到的 operator new 标準庫函數,傳入的參數為 class A 的大小,這裡為 8 個位元組,至于為什麼是 8 個位元組,你可以看看《深入 C++ 對象模型》一書,這裡不做多解釋。這樣函數傳回的是配置設定記憶體的起始位址,這裡假設是 0x007da290。
- 上面配置設定的記憶體是未初始化的,也是未類型化的,第二步就在這一塊原始的記憶體上對類對象進行初始化,調用的是相應的構造函數,這裡是調用
這個函數,從圖中也可以看到對這塊申請的記憶體進行了初始化,A:A(10);
。var=10, file 指向打開的檔案
- 最後一步就是傳回新配置設定并構造好的對象的指針,這裡 pA 就指向 0x007da290 這塊記憶體,pA 的類型為類 A 對象的指針。
所有這三步,你都可以通過反彙編找到相應的彙編代碼,在這裡我就不列出了。
好了,那麼 delete 都幹了什麼呢?還是接着上面的例子,如果這時想釋放掉申請的類的對象怎麼辦?當然我們可以使用下面的語句來完成:
delete 所做的事情如下圖所示:
delete 就做了兩件事情:
- 調用 pA 指向對象的析構函數,對打開的檔案進行關閉。
- 通過上面提到的标準庫函數 operator delete 來釋放該對象的記憶體,傳入函數的參數為 pA 的值,也就是 0x007d290。
好了,解釋完了 new 和 delete 背後所做的事情了,是不是覺得也很簡單?不就多了一個構造函數和析構函數的調用嘛。
如何申請和釋放一個數組?
我們經常要用到動态配置設定一個數組,也許是這樣的:
string *psa = new string[10]; //array of 10 empty strings
int *pia = new int[10]; //array of 10 uninitialized ints
上面在申請一個數組時都用到了
new []
這個表達式來完成,按照我們上面講到的 new 和 delete 知識,第一個數組是 string 類型,配置設定了儲存對象的記憶體空間之後,将調用 string 類型的預設構造函數依次初始化數組中每個元素;第二個是申請具有内置類型的數組,配置設定了存儲 10 個 int 對象的記憶體空間,但并沒有初始化。
如果我們想釋放空間了,可以用下面兩條語句:
delete [] psa;
delete [] pia;
都用到
delete []
表達式,注意這地方的 [] 一般情況下不能漏掉!我們也可以想象這兩個語句分别幹了什麼:第一個對 10 個 string 對象分别調用析構函數,然後再釋放掉為對象配置設定的所有記憶體空間;第二個因為是内置類型不存在析構函數,直接釋放為 10 個 int 型配置設定的所有記憶體空間。
這裡對于第一種情況就有一個問題了:我們如何知道 psa 指向對象的數組的大小?怎麼知道調用幾次析構函數?
這個問題直接導緻我們需要在 new [] 一個對象數組時,需要儲存數組的次元,C++ 的做法是在配置設定數組空間時多配置設定了 4 個位元組的大小,專門儲存數組的大小,在 delete [] 時就可以取出這個儲存的數,就知道了需要調用析構函數多少次了。
還是用圖來說明比較清楚,我們定義了一個類 A,但不具體描述類的内容,這個類中有顯示的構造函數、析構函數等。那麼 當我們調用
時需要做的事情如下:
從這個圖中我們可以看到申請時在數組對象的上面還多配置設定了 4 個位元組用來儲存數組的大小,但是最終傳回的是對象數組的指針,而不是所有配置設定空間的起始位址。
這樣的話,釋放就很簡單了:
這裡要注意的兩點是:
- 調用析構函數的次數是從數組對象指針前面的 4 個位元組中取出;
- 傳入
函數的參數不是數組對象的指針 pAa,而是 pAa 的值減 4。operator delete[]
為什麼 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[] 要配套使用總是沒錯的!