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 表达式中推导 |
允许推导 |