new
表示式
建立並初始化具有動態儲存期的物件,即生命期不一定受其建立範圍限制的物件。
目錄 |
[編輯] 語法
:: (可選) new ( 型別 ) new-initializer (可選) |
(1) | ||||||||
:: (可選) new 型別 new-initializer (可選) |
(2) | ||||||||
:: (可選) new ( placement-args ) ( 型別 ) new-initializer (可選) |
(3) | ||||||||
:: (可選) new ( placement-args ) 型別 new-initializer (可選) |
(4) | ||||||||
[編輯] 解釋
型別 | - | 目標型別識別符號 |
new-initializer | - | 一個用括號括起來的表示式列表或用花括號括起來的初始化器列表(C++11 起) |
placement-args | - | 額外的定位引數 |
new 表示式嘗試分配儲存,然後嘗試在分配的儲存中構造並初始化一個無名物件或一個無名物件陣列。new 表示式返回一個指向已構造物件的純右值指標,如果構造的是物件陣列,則返回指向陣列初始元素的指標。
如果 型別 包含括號,則需要使用語法 (1) 或 (3)。
new int(*[10])(); // error: parsed as (new int) (*[10]) () new (int (*[10])()); // okay: allocates an array of 10 pointers to functions
此外,型別 會被貪婪地解析:它將包含所有可以作為宣告符一部分的標記。
new int + 1; // okay: parsed as (new int) + 1, increments a pointer returned by new int new int * 1; // error: parsed as (new int*) (1)
如果滿足以下條件,則 new-initializer 不可省略:
- 型別 是未知邊界的陣列,
(C++11 起) | |
|
(C++17 起) |
double* p = new double[]{1, 2, 3}; // creates an array of type double[3] auto p = new auto('c'); // creates a single object of type char. p is a char* auto q = new std::integral auto(1); // OK: q is an int* auto q = new std::floating_point auto(true) // ERROR: type constraint not satisfied auto r = new std::pair(1, true); // OK: r is a std::pair<int, bool>* auto r = new std::vector; // ERROR: element type can't be deduced
[編輯] 動態陣列
如果 型別 是陣列型別,則除第一個維度之外的所有維度都必須指定為正的整型常量表達式(C++14 前)轉換為 std::size_t 型別的常量表達式(C++14 起),但(僅在使用非括號語法 (2) 和 (4) 時)第一個維度可以是整型、列舉型別或具有單個非顯式轉換函式到整型或列舉型別的類型別表示式(C++14 前)任何可轉換為 std::size_t 的表示式(C++14 起)。這是直接建立在執行時定義大小的陣列的唯一方法,此類陣列通常被稱為動態陣列。
int n = 42; double a[n][5]; // error auto p1 = new double[n][5]; // OK auto p2 = new double[5][n]; // error: only the first dimension may be non-constant auto p3 = new (double[n][5]); // error: syntax (1) cannot be used for dynamic arrays
如果第一個維度中的值(如果需要,轉換為整型或列舉型別)為負,則行為未定義。 |
(C++11 前) |
在以下情況下,指定第一個維度的表示式值無效:
如果第一個維度中的值由於上述任何原因無效,則:
|
(C++11 起) |
第一個維度為零是允許的,並且會呼叫分配函式。
如果 new-initializer 是花括號初始化器列表,並且第一個維度可能被求值且不是核心常量表達式,則會檢查從空初始化器列表複製初始化陣列假設元素的語義約束。 |
(C++11 起) |
[編輯] 分配
new 表示式透過呼叫適當的分配函式來分配儲存。如果 型別 是非陣列型別,則函式名為 operator new。如果 型別 是陣列型別,則函式名為 operator new[]。
如分配函式中所述,C++ 程式可以為這些函式提供全域性和類特定的替換。如果 new 表示式以可選的 :: 運算子開頭,如 ::new T 或 ::new T[n],則類特定的替換將被忽略(該函式在全域性作用域中查詢)。否則,如果 T
是類型別,則查詢從 T
的類作用域開始。
呼叫分配函式時,new 表示式將請求的位元組數作為第一個型別為 std::size_t 的引數傳遞,對於非陣列 T
,這恰好是 sizeof(T)。
陣列分配可能提供未指定的開銷,此開銷在每次 new 呼叫之間可能有所不同,除非所選分配函式是標準非分配形式。new 表示式返回的指標將與分配函式返回的指標偏移該值。許多實現使用陣列開銷來儲存陣列中的物件數量,delete[] 表示式使用此數量來呼叫正確數量的解構函式。此外,如果 new 表示式用於分配 char、unsigned char 或 std::byte(C++17 起) 的陣列,如果需要,它可能會從分配函式請求額外的記憶體,以確保所有型別不大於請求陣列大小的物件都能正確對齊(如果稍後將一個物件放入已分配的陣列中)。
new 表示式可以省略或合併透過可替換分配函式進行的分配。在省略的情況下,儲存可能由編譯器提供,而無需呼叫分配函式(這也允許最佳化掉未使用的 new 表示式)。在合併的情況下,new 表示式 E1 的分配可以擴充套件以提供額外的儲存給另一個 new 表示式 E2,如果滿足以下所有條件: 1) E1 分配的物件的生命期嚴格包含 E2 分配的物件的生命期。
2) E1 和 E2 將呼叫相同的可替換全域性分配函式。
3) 對於丟擲異常的分配函式,E1 和 E2 中的異常將在同一個處理程式中首次捕獲。
請注意,此最佳化僅在使用 new 表示式時才允許,而不是透過任何其他方法呼叫可替換分配函式:delete[] new int[10]; 可以被最佳化掉,但 operator delete(operator new(10)); 則不能。 |
(C++14 起) |
在求值常量表達式期間,對分配函式的呼叫總是被省略。只有原本會導致呼叫可替換全域性分配函式的 new 表示式才能在常量表達式中求值。 |
(C++20 起) |
[編輯] 定位 new
如果提供了 placement-args,它們將作為附加引數傳遞給分配函式。此類分配函式被稱為“定位 new”,得名於標準分配函式 void* operator new(std::size_t, void*),它只是原封不動地返回其第二個引數。這用於在已分配的儲存中構造物件。
// within any block scope... { // Statically allocate the storage with automatic storage duration // which is large enough for any object of type “T”. alignas(T) unsigned char buf[sizeof(T)]; T* tptr = new(buf) T; // Construct a “T” object, placing it directly into your // pre-allocated storage at memory address “buf”. tptr->~T(); // You must **manually** call the object's destructor // if its side effects is depended by the program. } // Leaving this block scope automatically deallocates “buf”.
注意:此功能由分配器(Allocator)類的成員函式封裝。
當分配其對齊要求超過 __STDCPP_DEFAULT_NEW_ALIGNMENT__ 的物件或此類物件的陣列時,new 表示式將對齊要求(封裝在 std::align_val_t 中)作為分配函式的第二個引數傳遞(對於定位形式,placement-arg 出現在對齊引數之後,作為第三、第四等引數)。如果過載解析失敗(當定義了具有不同簽名的類特定分配函式時發生,因為它隱藏了全域性函式),則會嘗試第二次過載解析,此時引數列表中不包含對齊引數。這允許對齊不敏感的類特定分配函式優先於全域性對齊敏感的分配函式。 |
(C++17 起) |
new T; // calls operator new(sizeof(T)) // (C++17) or operator new(sizeof(T), std::align_val_t(alignof(T)))) new T[5]; // calls operator new[](sizeof(T)*5 + overhead) // (C++17) or operator new(sizeof(T)*5+overhead, std::align_val_t(alignof(T)))) new(2,f) T; // calls operator new(sizeof(T), 2, f) // (C++17) or operator new(sizeof(T), std::align_val_t(alignof(T)), 2, f)
如果一個不丟擲異常的分配函式(例如透過 new(std::nothrow) T 選擇的那個)由於分配失敗而返回空指標,則 new 表示式立即返回,它不會嘗試初始化物件或呼叫 deallocation 函式。如果將空指標作為引數傳遞給不分配記憶體的定位 new 表示式,這會使所選的標準不分配記憶體的定位分配函式返回空指標,則行為是未定義的。
[編輯] 初始化
由 new 表示式建立的物件根據以下規則初始化。
如果 型別 不是陣列型別,則單個物件在獲得的記憶體區域中構造:
|
(C++11 起) |
如果 型別 是陣列型別,則初始化物件陣列:
- 如果 new-initializer 不存在,則每個元素都預設初始化。
- 即使第一個維度為零,仍然需要滿足預設初始化假設元素的語義約束。
- 如果 new-initializer 是一對括號,則每個元素都值初始化。
- 即使第一個維度為零,仍然需要滿足值初始化假設元素的語義約束。
|
(C++11 起) |
|
(C++20 起) |
[編輯] 初始化失敗
如果初始化透過丟擲異常(例如來自建構函式)而終止,則程式會查詢匹配的 deallocation 函式,然後:
- 如果可以找到合適的 deallocation 函式,則呼叫該 deallocation 函式來釋放正在構造物件的記憶體。之後,異常在 new 表示式的上下文中繼續傳播。
- 如果找不到明確匹配的 deallocation 函式,則傳播異常不會導致物件記憶體被釋放。這僅在被呼叫的分配函式不分配記憶體時才適用,否則很可能會導致記憶體洩漏。
匹配 deallocation 函式的查詢範圍確定如下:
- 如果 new 表示式不以
::
開頭,且分配型別是類型別T
或類型別T
的陣列,則在T
的類作用域中搜索 deallocation 函式的名稱。 - 否則,或者如果在
T
的類作用域中未找到任何內容,則透過在全域性作用域中搜索來查詢 deallocation 函式的名稱。
對於非定位分配函式,使用正常的 deallocation 函式查詢來查詢匹配的 deallocation 函式(參見delete-expression)。
對於定位分配函式,匹配的 deallocation 函式必須具有相同數量的引數,並且除了第一個引數之外的每個引數型別都與分配函式的相應引數型別相同(在引數轉換之後)。
- 如果查詢找到一個匹配的 deallocation 函式,則將呼叫該函式;否則,將不呼叫任何 deallocation 函式。
- 如果查詢找到一個非定位 deallocation 函式,並且該函式作為定位 deallocation 函式,本應被選為分配函式的匹配,則程式格式錯誤。
在任何情況下,匹配的 deallocation 函式(如果有)必須是未刪除且(C++11 起)在 new 表示式出現的位置可訪問的。
struct S { // Placement allocation function: static void* operator new(std::size_t, std::size_t); // Non-placement deallocation function: static void operator delete(void*, std::size_t); }; S* p = new (0) S; // error: non-placement deallocation function matches // placement allocation function
如果在 new 表示式中呼叫 deallocation 函式(由於初始化失敗),則傳遞給該函式的引數確定如下:
- 第一個引數是從分配函式呼叫返回的值(型別為 void*)。
- 其他引數(僅適用於定位 deallocation 函式)是傳遞給定位分配函式的 placement-args。
如果允許實現引入臨時物件或複製任何引數作為呼叫分配函式的一部分,則未指定是否在分配函式和 deallocation 函式的呼叫中使用相同的物件。
[編輯] 記憶體洩漏
由 new 表示式建立的物件(具有動態儲存期的物件)會一直存在,直到 new 表示式返回的指標在匹配的delete-expression中使用。如果指標的原始值丟失,物件將無法訪問且無法釋放:發生記憶體洩漏。
這可能在以下情況下發生:指標被賦值,
int* p = new int(7); // dynamically allocated int with value 7 p = nullptr; // memory leak
或者指標超出作用域,
void f() { int* p = new int(7); } // memory leak
或者由於異常。
void f() { int* p = new int(7); g(); // may throw delete p; // okay if no exception } // memory leak if g() throws
為了簡化動態分配物件的管理,new 表示式的結果通常儲存在智慧指標中:std::auto_ptr (C++17 前)std::unique_ptr 或 std::shared_ptr(C++11 起)。這些指標保證在上述情況下執行 delete 表示式。
[編輯] 注意
Itanium C++ ABI 要求如果建立的陣列的元素型別是 trivially destructible,則陣列分配開銷為零。MSVC 也是如此。
一些實現(例如 VS 2019 v16.7 之前的 MSVC)要求在非分配定位陣列 new 中,如果元素型別不是 trivially destructible,則陣列分配開銷非零,這自 CWG 問題 2382 起不再符合標準。
一個非分配定位陣列 new 表示式,它建立一個 unsigned char 或 std::byte(C++17 起) 的陣列,可用於在給定儲存區域上隱式建立物件:它結束與陣列重疊的物件的生命期,然後隱式建立陣列中具有隱式生命期型別的物件。
std::vector 為一維動態陣列提供了類似的功能。
[編輯] 關鍵詞
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。
缺陷報告 | 應用於 | 釋出時的行為 | 正確的行為 |
---|---|---|---|
CWG 74 | C++98 | 第一個維度中的值必須為整型 | 允許列舉型別 |
CWG 299 | C++98 | 第一個維度中的值必須 具有整型或列舉型別 |
允許具有單個 到整型或列舉型別的 轉換函式的類型別 |
CWG 624 | C++98 | 當分配物件的大小 超過實現定義限制時, 行為未指定 |
在這種情況下不獲取儲存, 並丟擲異常 |
CWG 1748 | C++98 | 非分配定位 new 需要 檢查引數是否為 null |
null 引數導致未定義行為 |
CWG 1992 | C++11 | new (std::nothrow) int[N] 可能丟擲 std::bad_array_new_length |
改為返回空指標 |
CWG 2102 | C++98 | 不清楚初始化空陣列時 是否需要預設/值初始化為良構 |
需要 |
CWG 2382 | C++98 | 非分配定位陣列 new 可能需要分配開銷 |
此類分配開銷被禁止 |
CWG 2392 | C++11 | 即使第一個維度未被潛在求值, 程式也可能格式錯誤 |
在這種情況下是良構的 |
P1009R2 | C++11 | 陣列邊界無法在 new 表示式中推導 |
允許推導 |