new
表达式
创建并初始化具有动态存储期的对象,即,对象的生命周期不一定受限于创建它们的作用域。
内容 |
[编辑] 语法
:: (可选) new ( type) new-initializer(可选) |
(1) | ||||||||
:: (可选) new type new-initializer(可选) |
(2) | ||||||||
:: (可选) new ( placement-args) ( type) new-initializer(可选) |
(3) | ||||||||
:: (可选) new ( placement-args) type new-initializer(可选) |
(4) | ||||||||
[编辑] 解释
type | - | 目标类型标识符 |
new-initializer | - | 圆括号包围的表达式列表 或花括号包围的初始化器列表(自 C++11 起) |
placement-args | - | 额外的 placement 实参 |
new 表达式尝试分配存储,然后尝试在已分配的存储中构造和初始化单个未命名对象或未命名对象数组。new 表达式返回指向已构造对象的纯右值指针,或者,如果构造了对象数组,则返回指向数组初始元素的指针。
如果 type 包含圆括号,则需要语法 (1) 或 (3)
new int(*[10])(); // error: parsed as (new int) (*[10]) () new (int (*[10])()); // okay: allocates an array of 10 pointers to functions
此外,type 被贪婪地解析:它将包含每个可以作为声明符一部分的标记
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 不是可选的:
- type 是未知边界数组,
(自 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
[编辑] 动态数组
如果 type 是数组类型,则除第一维之外的所有维度都必须指定为正整数常量表达式(直到 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 表达式通过调用适当的分配函数来分配存储。如果 type 是非数组类型,则函数的名称为 operator new。如果 type 是数组类型,则函数的名称为 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 起) |
[编辑] Placement new
如果提供了 placement-args,则它们将作为额外的参数传递给分配函数。此类分配函数被称为 “placement 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 形式,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 表达式立即返回,它不会尝试初始化对象或调用释放分配函数。如果将空指针作为参数传递给非分配 placement new 表达式,这使得选定的标准非分配 placement 分配函数返回空指针,则行为未定义。
[编辑] 初始化
由 new 表达式创建的对象根据以下规则初始化。
如果 type 不是数组类型,则在获得的内存区域中构造单个对象
|
(自 C++11 起) |
如果 type 是数组类型,则初始化对象数组
- 如果 new-initializer 缺失,则每个元素都被默认初始化。
- 即使第一维为零,仍然需要满足默认初始化假设元素的语义约束。
- 如果 new-initializer 是一对圆括号,则每个元素都被值初始化。
- 即使第一维为零,仍然需要满足值初始化假设元素的语义约束。
|
(自 C++11 起) |
|
(自 C++20 起) |
[编辑] 初始化失败
如果初始化因抛出异常而终止(例如,从构造函数中),则程序查找匹配的释放分配函数,然后
- 如果可以找到合适的释放分配函数,则调用释放分配函数以释放正在构造对象的内存。之后,异常继续在 new 表达式的上下文中传播。
- 如果找不到明确匹配的释放分配函数,则传播异常不会导致对象的内存被释放。仅当调用的分配函数不分配内存时,这才适用,否则很可能导致内存泄漏。
匹配的释放分配函数的查找作用域确定如下
- 如果 new 表达式不以
::
开头,并且分配的类型是类类型T
或类类型T
的数组,则在T
的类作用域中搜索释放分配函数的名称。 - 否则,如果在
T
的类作用域中未找到任何内容,则通过在全局作用域中搜索来查找释放分配函数的名称。
对于非 placement 分配函数,使用正常的释放分配函数查找来查找匹配的释放分配函数(参见 delete 表达式)。
对于 placement 分配函数,匹配的释放分配函数必须具有相同数量的参数,并且除第一个参数之外的每个参数类型都与分配函数的相应参数类型相同(在参数转换之后)。
- 如果查找找到单个匹配的释放分配函数,则将调用该函数;否则,将不调用释放分配函数。
- 如果查找找到非 placement 释放分配函数,并且该函数(被视为 placement 释放分配函数)将被选为分配函数的匹配项,则程序是非良构的。
在任何情况下,匹配的释放分配函数(如果有)都必须是非删除的,并且(自 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 表达式中调用了释放分配函数(由于初始化失败),则传递给该函数的参数确定如下
- 第一个参数是从分配函数调用返回的值(类型为 void*)。
- 其他参数(仅适用于 placement 释放分配函数)是传递给 placement 分配函数的 placement-args。
如果允许实现引入临时对象或复制任何参数作为调用分配函数的一部分,则不指定在调用分配函数和释放分配函数时是否使用同一对象。
[编辑] 内存泄漏
由 new 表达式创建的对象(具有动态存储期的对象)将持续存在,直到 new 表达式返回的指针在匹配的 delete 表达式中使用。如果指针的原始值丢失,则对象将变得不可访问且无法释放分配:发生内存泄漏。
如果指针被赋值给,则可能发生这种情况
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 要求,如果创建的数组的元素类型是平凡可析构的,则数组分配开销为零。MSVC 也是如此。
某些实现(例如,VS 2019 v16.7 之前的 MSVC)在非分配 placement 数组 new 上需要非零数组分配开销,如果元素类型不是平凡可析构的,则不再符合 CWG issue 2382 的规定。
创建 unsigned char或 std::byte(自 C++17 起) 数组的非分配 placement 数组 new 表达式可用于在给定的存储区域上隐式创建对象:它结束与数组重叠的对象的生命周期,然后在数组中隐式创建隐式生命周期类型的对象。
std::vector 为一维动态数组提供类似的功能。
[编辑] 关键字
[编辑] 缺陷报告
以下行为变更缺陷报告已追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
CWG 74 | C++98 | 第一维中的值必须具有整数类型 | 允许枚举类型 |
CWG 299 | C++98 | 第一维的值必须 具有整型或枚举类型 |
具有单个转换函数的类类型 到整型 或枚举类型是允许的 |
CWG 624 | C++98 | 当分配对象的大小超过 实现定义的限制时,行为是未指定的 实现定义的限制 |
不获取存储,并且 在这种情况下抛出异常 |
CWG 1748 | C++98 | 非分配式 placement new 需要 检查参数是否为空 |
空参数的未定义行为 |
CWG 1992 | C++11 | new (std::nothrow) int[N] 可能抛出 std::bad_array_new_length 异常 |
已更改为返回空指针 |
CWG 2102 | C++98 | 默认/值初始化是否 在初始化空数组时需要良好定义,这一点尚不清楚 |
需要 |
CWG 2382 | C++98 | 非分配式 placement 数组 new 可能需要分配开销 |
不允许此类分配开销 |
CWG 2392 | C++11 | 即使 第一维不是潜在求值的,程序也可能是不良形式的 |
在这种情况下是良好形式的 |
P1009R2 | C++11 | 数组边界可能无法 在 new 表达式中推导 |
允许推导 |