在 C++ 中,你也許常用 new 和 delete 來動態申請和釋放內存,但你可曾想過如下問題呢?html
若是你對這些問題都有疑問的話,不妨看看我這篇文章。git
若是找工做的同窗看一些面試的書,我相信都會遇到這樣的題:sizeof 不是函數,而後舉出一堆的理由來證實 sizeof 不是函數。在這裏,和 sizeof 相似,new 和 delete 也不是函數,它們都是 C++ 定義的關鍵字,經過特定的語法能夠組成表達式。和 sizeof 不一樣的是,sizeof 在編譯時候就能夠肯定其返回值,new 和 delete 背後的機制則比較複雜。
繼續往下以前,請你想一想你認爲 new 應該要作些什麼?也許你第一反應是,new 不就和 C 語言中的 malloc 函數同樣嘛,就用來動態申請空間的。你答對了一半,看看下面語句:github
string *ps = new string("hello world");
你就能夠看出 new 和 malloc 仍是有點不一樣的,malloc 申請完空間以後不會對內存進行必要的初始化,而 new 能夠。因此 new expression 背後要作的事情不是你想象的那麼簡單。在我用實例來解釋 new 背後的機制以前,你須要知道 operator new
和 operator delete
是什麼玩意。面試
這兩個實際上是 C++ 語言標準庫的庫函數,原型分別以下:express
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 申請內存以後不對內存進行初始化,直接返回申請內存的指針。數組
咱們能夠直接在咱們的程序中使用這幾個函數。markdown
知道上面兩個函數以後,咱們用一個實例來解釋 new 和 delete 背後的機制:函數
咱們不用簡單的 C++ 內置類型來舉例,使用複雜一點的類類型,定義一個類 A:post
class A { public: A(int v) : var(v) { fopen_s(&file, "test", "r"); } ~A() { fclose(file); } private: int var; FILE *file; };
很簡單,類 A 中有兩個私有成員,有一個構造函數和一個析構函數,構造函數中初始化私有變量 var 以及打開一個文件,析構函數關閉打開的文件。url
咱們使用
class A *pA = new A(10);
來建立一個類的對象,返回其指針 pA。以下圖所示 new 背後完成的工做:
簡單總結一下:
A:A(10);
這個函數,從圖中也能夠看到對這塊申請的內存進行了初始化,var=10, file 指向打開的文件
。全部這三步,你均可以經過反彙編找到相應的彙編代碼,在這裏我就不列出了。
好了,那麼 delete 都幹了什麼呢?仍是接着上面的例子,若是這時想釋放掉申請的類的對象怎麼辦?固然咱們可使用下面的語句來完成:
delete pA;
delete 所作的事情以下圖所示:
delete 就作了兩件事情:
好了,解釋完了 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,但不具體描述類的內容,這個類中有顯示的構造函數、析構函數等。那麼 當咱們調用
class A *pAa = new A[3];
時須要作的事情以下:
從這個圖中咱們能夠看到申請時在數組對象的上面還多分配了 4 個字節用來保存數組的大小,可是最終返回的是對象數組的指針,而不是全部分配空間的起始地址。
這樣的話,釋放就很簡單了:
delete []pAa;
這裏要注意的兩點是:
operator delete[]
函數的參數不是數組對象的指針 pAa,而是 pAa 的值減 4。其實說了這麼多,還沒到我寫這篇文章的最原始意圖。從上面解釋的你應該懂了 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;
作了兩件事:
operator delete(pAa);
釋放內存。顯然,這裏只對數組的第一個類對象調用了析構函數,後面的兩個對象均沒調用析構函數,若是類對象中申請了大量的內存須要在析構函數中釋放,而你卻在銷燬數組對象時少調用了析構函數,這會形成內存泄漏。
上面的問題你若是說不要緊的話,那麼第二點就是致命的了!直接釋放 pAa 指向的內存空間,這個老是會形成嚴重的段錯誤,程序必然會奔潰!由於分配的空間的起始地址是 pAa 指向的地方減去 4 個字節的地方。你應該傳入參數設爲那個地址!
同理,你能夠分析若是使用 new 來分配,用 delete []
來釋放會出現什麼問題?是否是總會致使程序錯誤?
總的來講,記住一點便可:new/delete、new[]/delete[] 要配套使用老是沒錯的!
C++ Primer 第四版