C++中new和delete之後發生了什麼
眾所周知,如果我們使用new向系統申請了記憶體,我們應該使用指標指向這一塊記憶體,俾能我們使用結束後,通過delete該指標釋放此記憶體資源。
如果理解只達到這種程度,在記憶體管理稍微複雜一點時便一定會束手無策。總有一些事情比其他事情更基本一點,現在我來談談當我們new和delete之後到底發生了什麼。
C++中的五種記憶體
在C++中記憶體分為五個區:堆、棧、自由儲存區、全域性/靜態儲存區和常量儲存區。
- 堆區:使用者使用new獲得的記憶體在這裡。使用者需要自行管理其宣告週期,也就是說一個new要對應一個delete,如果因為某些原因(之後我會說明一些可能的原因)記憶體沒有被釋放,那麼在程式結束後,會由作業系統自行回收,這顯然不是我們想看到的。
- 棧區:儲存區域性變數、函式引數等,比方說你在某個函式裡定義了一個int變數a,這個a就存放在棧區。這塊記憶體的生命週期由系統管理,不需要我們去操心。
- 自由儲存區:用malloc分配的記憶體放置在這裡。這塊記憶體和堆很相似,不過是使用free來釋放記憶體的。
- 全域性/靜態儲存區:存放全域性變數和靜態變數。
- 常量儲存區:存放常量,不允許更改。
new和delete
回到我們的主題。先看一段程式碼:
int *p = new int;
cout << *p << endl;//輸出-842150451
cout << &p << endl;//輸出004FFC14
*p = 1;
cout << *p << endl;//輸出1
cout << &p << endl;//輸出004FFC14
delete p;
cout << *p << endl;//輸出-572662307
cout << &p << endl;//輸出004FFC14
首先聲明瞭一個整形指標指向我們新開闢的記憶體,但沒有將其顯示初始化也沒有為其賦值。
輸出*p顯示為-842150451。嗯,看起來這是編譯器為我們預設初始化的值。
輸出&p顯示為004FFC14,現在我們知道了我們開闢的記憶體在哪裡。
接下來我們將p指向的值定義為1。
輸出*p顯示為1,很好,這正是我們所期望的。
輸出&p顯示為004FFC14,記憶體地址沒有變化。
接著我們delete p,之後發生了什麼呢?
輸出*p顯示-572662307。對p呼叫delete後我們仍然能取到一個值!
輸出&p顯示004FFC14。哇!還是原來的地址。
從此我們可以看出,delete指標並非將該指標棄置不用,而是將其指向的記憶體中的資料清除,但是指標仍然指向原來的記憶體!
那麼如果我們想按照delete的英文字意,把這個指標從世界上徹底銷燬,需要怎麼做呢?
p = nullptr;
cout << *p << endl;//程式到此停止執行
cout << &p << endl;
將p的值賦為nullptr,現在這個指標才被銷燬了。注意這裡取一個空指標的地址和值的行為,其結果將是未定義的。
神奇的定值
我發現申請相同型別的記憶體時,編譯器都會分配給其一個定製,對於int該值為前面提到的-842150451。同樣delete掉指標後,也會有一個定值為-572662307。我分析了一下其原碼和補碼,沒發現有什麼特殊的,如果你知道這些數字的意義請留言告訴我,謝謝。
關於動態陣列
如果要動態分配一個數組,要在型別名後跟一對方括號,在其中指明要分配的物件的數目,其型別必須是整形但不必是常量。其返回值為指向陣列第一個物件的指標。
int* p = new int[get_size()];
注意這裡分配的記憶體世界上並不是一個數組型別(也不存在這樣的型別),因此不能對動態陣列呼叫begin或end,也不能使用範圍
for語句來處理其中的元素。
為了釋放動態陣列,我們要使用一種特殊形式的delete——在指標前面加上一個空方括號。
delete [] p;
如果這裡我們忘記了方括號,其結果將是未定義的。
再深入一點
class A {
;
};
A* pA = new A;
delete pA;
這裡發生了什麼呢?實際上,這段程式裡面隱含呼叫了一些我們沒有看到的東西,那就是:
static void* operator new(size_t sz);
static void operator delete(void* p);
值得注意的是,這兩個函式都是static的,所以如果我們過載了這2個函式(我們要麼不過載,要過載就要2個一起行動),也應該宣告為static的,如果我們沒有宣告,系統也會為我們自動加上。另外,這是兩個記憶體分配原語,要麼成功,要麼沒有分配任何記憶體。
回到主題,new A;
實際上做了2件事:呼叫opeator new,在自由儲存區分配一個sizeof(A)大小的記憶體空間;然後呼叫建構函式A(),在這塊記憶體空間上類磚砌瓦,建造起我們的物件。同樣對於delete,則做了相反的兩件事:呼叫解構函式~A(),銷燬物件,呼叫operator delete,釋放記憶體。
使用new_handler處理異常
當operator new無法滿足某一記憶體分配需求是,它會丟擲異常。某些舊式編譯器會在此時返回一個null指標,但是現在我們可以使用new_handler定製異常處理行為。
new_handler是個typedef,定義一個指標指向函式,該函式沒有引數也不返回任何東西。
我們使用set_new_handler函式,其引數是個指標,指向operator new無法分配足夠記憶體是該被呼叫的函式,其返回值也是個指標,指向set_new_handler被呼叫前正在執行(但馬上就要被替換)的那個new_handler函式。
更詳盡的內容推薦閱讀《Effective C++》一書的條款49。
使用智慧指標
如果可以,我們應該使用STL提供的shared_ptr和unique_ptr替換原始指標,這樣我們可以不用自行管理記憶體的生命週期,獲得類似JAVA和C#的自動記憶體回收體驗。
shared_ptr<int> p = make_shared<int>(1);
shared_ptr<int> q(new int(2));
注意:
- 不要使用相同的內建指標初始化(或reset)多個智慧指標。
- 不delete get()返回的指標
- 不適用get()初始化或reset另一個智慧指標
- 如果使用了get()返回的指標,記住當最後一個對應的智慧指標銷燬後,你的指標就變為無效了。
- 不過你使用智慧指標管理的資源不是new分配的記憶體,記住傳遞給它一個刪除器。
allocator類
該類幫助我們將記憶體分配和物件構造分離開來,它分配的記憶體是原始的、未構造的。
allocator<string> alloc;//可以分配string的allocator物件
auto const p = alloc.allocate(n);//分配n個未初始化的string
malloc/free
從C程式設計師轉換過來的C++程式設計師總是有個困惑:new/delete到底究竟和C語言裡面的malloc/free比起來有什麼優勢?或者是一樣的?
- malloc/free只是對記憶體進行分配和釋放;new/delete還負責完成了建立和銷燬物件的任務。
- new的安全性要高一些,因為他返回的就是一個所建立的物件的指標,對於malloc來說返回的則是void*,還要進行強制型別轉換,顯然這是一個危險的漏洞。
- 我們可以對new/delete過載,使記憶體分配按照我們的意願進行,這樣更具有靈活性,malloc則不行。
不過,new/delete也並不是十分完美,大概最大的缺點就是效率低(針對的是預設的分配器),原因不只是因為在自由儲存區上分配(和棧上對比),而且new只是對於堆分配器(malloc/realloc/free)的一個淺層包裝,沒有針對小型的記憶體分配做優化。另外預設分配器具有通用性,它管理的是一塊記憶體池,這樣的管理往往需要消耗一些額外空間。我們可以針對new/delete進行重寫以追求更高的效率,對於這方面更深入的探討可以參考《Effective C++》第八章。