C++——new和delete之後發生了什麼?

衆所周知,如果我們使用new向系統申請了內存,我們應該使用指針指向這一塊內存,俾能我們使用結束後,通過delete該指針釋放此內存資源。

如果理解只達到這種程度,在內存管理稍微複雜一點時便一定會束手無策。總有一些事情比其他事情更基本一點,現在我來談談當我們new和delete之後到底發生了什麼。

C++中的五種內存

在C++中內存分爲五個區:堆、棧、自由存儲區、全局/靜態存儲區和常量存儲區。

  1. 堆區:用戶使用new獲得的內存在這裏。用戶需要自行管理其聲明週期,也就是說一個new要對應一個delete,如果因爲某些原因(之後我會說明一些可能的原因)內存沒有被釋放,那麼在程序結束後,會由操作系統自行回收,這顯然不是我們想看到的。
  2. 棧區:存儲局部變量、函數參數等,比方說你在某個函數裏定義了一個int變量a,這個a就存放在棧區。這塊內存的生命週期由系統管理,不需要我們去操心。
  3. 自由存儲區:用malloc分配的內存放置在這裏。這塊內存和堆很相似,不過是使用free來釋放內存的。
  4. 全局/靜態存儲區:存放全局變量和靜態變量。
  5. 常量存儲區:存放常量,不允許更改。

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));

注意:

  1. 不要使用相同的內置指針初始化(或reset)多個智能指針。
  2. 不delete get()返回的指針
  3. 不適用get()初始化或reset另一個智能指針
  4. 如果使用了get()返回的指針,記住當最後一個對應的智能指針銷燬後,你的指針就變爲無效了。
  5. 不過你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器。

allocator類

該類幫助我們將內存分配和對象構造分離開來,它分配的內存是原始的、未構造的。

allocator<string> alloc;//可以分配string的allocator對象
auto const p = alloc.allocate(n);//分配n個未初始化的string

malloc/free

從C程序員轉換過來的C++程序員總是有個困惑:new/delete到底究竟和C語言裏面的malloc/free比起來有什麼優勢?或者是一樣的?

  1. malloc/free只是對內存進行分配和釋放;new/delete還負責完成了創建和銷燬對象的任務。
  2. new的安全性要高一些,因爲他返回的就是一個所創建的對象的指針,對於malloc來說返回的則是void*,還要進行強制類型轉換,顯然這是一個危險的漏洞。
  3. 我們可以對new/delete重載,使內存分配按照我們的意願進行,這樣更具有靈活性,malloc則不行。

不過,new/delete也並不是十分完美,大概最大的缺點就是效率低(針對的是缺省的分配器),原因不只是因爲在自由存儲區上分配(和棧上對比),而且new只是對於堆分配器(malloc/realloc/free)的一個淺層包裝,沒有針對小型的內存分配做優化。另外缺省分配器具有通用性,它管理的是一塊內存池,這樣的管理往往需要消耗一些額外空間。我們可以針對new/delete進行重寫以追求更高的效率,對於這方面更深入的探討可以參考《Effective C++》第八章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章