摘要:“new”是C++的一個關鍵字,同時也是操作符。關於new的話題非常多,因為它確實比較複雜,也非常神祕。
本文分享自華為雲社群《如何編寫高效、優雅、可信程式碼系列(2)——你真的會用new嗎》,原文作者:我是一顆大西瓜 。
C++記憶體管理
1. C++記憶體分配
C++中的程式載入到記憶體後按照程式碼區、資料區、堆區、棧區進行佈局,其中資料區又可以分為自由儲存區、全域性/靜態儲存區和常量儲存區,各區所長如下:
- 棧區
函式執行的時候,區域性變數的儲存單元都在棧上建立,函式執行結束後儲存單元會自動釋放。棧記憶體分配運算內置於處理器指令集中,效率高,但分配記憶體容量有限。 - 堆區
堆就是new出來的記憶體塊,編譯器不管釋放,由應用程式控制,new對應delete。如果沒釋放掉,程式結束後,作業系統會自動回收。 - 自由儲存區
C中malloc分配的記憶體塊。用free結束生命週期。 - 全域性/靜態儲存區
全域性變數和靜態變數被分配到同一塊記憶體中,定義的時候就會初始化。 - 常量儲存區
比較特殊的儲存區,存放常量,不允許修改。
堆和棧的區別
- 管理方式
棧由編譯器自動管理,堆由程式設計師控制 - 空間大小
32位系統下,堆記憶體可以達到4GB,棧有一定的空間大小 - 碎片管理
對於堆,頻繁的new/delete肯定造成記憶體空間的不連續,產生大量記憶體碎片降低程式效率;棧由於遵循先進後出的規則,不會產生空隙 - 生長方向
堆是向上生長的,即向著記憶體地址增加的方向增長;而棧是向著記憶體地址減小的方向增長的 - 分配方式
堆是動態分配的,棧有動態分配和靜態分配之分:靜態分配由編譯器完成,動態分配由alloca函式完成,即使是動態分配,依然是編譯器自動釋放 - 分配效率
計算機底層提供了棧的支援,分配了專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這決定了棧的效率會比較高。堆則是由C/C++函式庫提供的,機制比較複雜,比如為了分配某個大小的記憶體需要在堆記憶體中搜索可用足夠大小的空間,效率比棧要低的多
2. new/delete和new []/delete []
- 回收new分配的單個物件記憶體空間時用delete,回收用new[]分配的一組物件時用delete[]
- 對於內建型別(int/double/float/char/…),由於new[]申請記憶體時,編譯器還會悄悄在記憶體中儲存整數,表示指標陣列的個數,所以delete/delete[]都可以正確釋放所申請的記憶體空間
- 建議在呼叫new時使用的[],那麼呼叫delete也使用[]
3. new的三種形態
- new operator 常用的new,語言函式內建,不能過載。呼叫過程中實際完成的有三件事:
- 為型別物件分配記憶體;
- 呼叫建構函式初始化記憶體物件;
- 返回物件指標
如果是在堆上建立物件,直接使用new operator。
- operator new 普通操作符,可以過載。如果僅僅是分配記憶體,那麼應該呼叫operator new,但不負責初始化。系統預設提供的分配器在時間和空間兩方面都存在一些問題:分配器速度較慢,分配小型物件時空間浪費嚴重,過載new/delete有三方面好處:
- 改善效率
- 檢測程式碼中的記憶體錯誤
- 獲得記憶體使用的統計資料
- C++標準規定,過載的operator new必須是類成員函式或全域性函式,全域性的operator new過載不應該改變原有簽名,而是直接無縫替換原有版本。全域性過載很有侵略性,別人使用你的庫無法使用預設的new,而具體類的過載只會影響本class和其派生類,但是類的operator new函式過載必須宣告為static,因為operator new是在類的具體物件被構建出來之前呼叫的。
- 為了獲得2和3的優勢,過載的operator new需要如下函式宣告void* operator new(size_t, const char* file, int line);
- placement new 定義在庫<<new>>中。如果想在一塊已經獲得記憶體裡建立物件,那麼應該呼叫placement new。通常情況不建議使用,但在某些對時間要求非常高的應用中可以考慮,因為選擇合適的建構函式完成物件初始化是一個時間相對較長的過程。