new
expression
创建并初始化具有动态 存储期 的对象,即其生存期不一定受限于创建时所处作用域的对象。
目录 |
语法
::
(可选)
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 | - | 附加的布局参数 |
new
表达式尝试分配存储空间,随后尝试在已分配的存储空间中构造并初始化单个无名对象或无名对象数组。
new
表达式返回一个指向被构造对象的纯右值指针,若构造的是对象数组,则返回指向数组首元素的指针。
当 type 包含括号时,需要使用 (1) 或 (3) 语法:
new int(*[10])(); // 错误:解析为 (new int) (*[10]) () new (int (*[10])()); // 正确:分配包含10个函数指针的数组
此外, type 会被贪婪解析:它将包含所有可能成为声明符组成部分的标记:
new int + 1; // 正确:解析为 (new int) + 1,对 new int 返回的指针进行递增 new int * 1; // 错误:解析为 (new int*) (1)
当 new-initializer 不可省略时
- type 是一个 未知边界数组 ,
| (C++11 起) | |
|
(C++17 起) |
double* p = new double[]{1, 2, 3}; // 创建一个 double[3] 类型的数组 auto p = new auto('c'); // 创建一个 char 类型的单个对象。p 是 char* auto q = new std::integral auto(1); // 正确:q 是 int* auto q = new std::floating_point auto(true) // 错误:类型约束不满足 auto r = new std::pair(1, true); // 正确:r 是 std::pair<int, bool>* auto r = new std::vector; // 错误:无法推导元素类型
动态数组
如果 type 是数组类型,除第一维外的所有维度必须指定为正的 整型常量表达式 (C++14 前) 转换为 std::size_t 类型的常量表达式 (C++14 起) ,但(仅当使用未加括号的语法 (2) 和 (4) 时)第一维可以是 整型、枚举类型或具有单个非显式转换函数到整型或枚举类型的类类型的表达式 (C++14 前) 可转换为 std::size_t 的任何表达式 (C++14 起) 。这是直接创建运行时定义大小数组的唯一方式,此类数组通常被称为 动态数组 :
int n = 42; double a[n][5]; // 错误 auto p1 = new double[n][5]; // 正确 auto p2 = new double[5][n]; // 错误:只有第一维度可以是非常量 auto p3 = new (double[n][5]); // 错误:语法(1)不能用于动态数组
|
若第一维的值(按需转换为整数或枚举类型)为负,则行为未定义。 |
(C++11 前) |
|
在以下情况下,指定第一维的表达式值无效:
若第一维的值因上述任一原因无效:
|
(C++11 起) |
第一个维度为零是可接受的,此时会调用分配函数。
|
如果 new-initializer 是大括号包围的初始化列表,且第一维度是 潜在求值 且非 核心常量表达式 ,则会检查从空初始化列表 拷贝初始化 数组假设元素的语义约束。 |
(since 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 起) |
布置 new
如果提供了 placement-args ,它们将作为额外参数传递给分配函数。这类分配函数被称为“placement new ”,其命名源于标准分配函数 void * operator new ( std:: size_t , void * ) ,该函数会直接返回其第二个参数而不作修改。此机制用于在已分配存储中构造对象:
// 在任何块作用域内... { // 以自动存储期静态分配存储空间 // 其大小足以容纳任何类型为“T”的对象 alignas(T) unsigned char buf[sizeof(T)]; T* tptr = new(buf) T; // 构造一个“T”对象,将其直接放置到 // 预分配的内存地址“buf”处的存储空间中 tptr->~T(); // 如果程序依赖该对象的副作用 // 则必须**手动**调用对象的析构函数 } // 离开此块作用域将自动释放“buf”
注意:此功能已由 Allocator 类的成员函数封装。
|
当分配对齐要求超过 __STDCPP_DEFAULT_NEW_ALIGNMENT__ 的对象或此类对象的数组时, new 表达式将对齐要求(包装在 std::align_val_t 中)作为分配函数的第二个参数传递(对于placement形式, placement-arg 在对齐参数之后出现,作为第三、第四等参数)。如果重载决议失败(当类特定分配函数以不同签名定义时会发生,因为它会隐藏全局函数),将尝试第二次重载决议,此时参数列表中不包含对齐参数。这使得不感知对齐的类特定分配函数能够优先于全局的感知对齐分配函数。 |
(since C++17) |
new T; // 调用 operator new(sizeof(T)) // (C++17) 或 operator new(sizeof(T), std::align_val_t(alignof(T)))) new T[5]; // 调用 operator new[](sizeof(T)*5 + overhead) // (C++17) 或 operator new(sizeof(T)*5+overhead, std::align_val_t(alignof(T)))) new(2,f) T; // 调用 operator new(sizeof(T), 2, f) // (C++17) 或 operator new(sizeof(T), std::align_val_t(alignof(T)), 2, f)
如果非抛出版本分配函数(例如通过 new ( std:: nothrow ) T 选择的函数)因分配失败而返回空指针,则 new 表达式会立即返回,不会尝试初始化对象或调用解分配函数。如果将空指针作为参数传递给非分配布置 new 表达式,导致选择的标准非分配布置分配函数返回空指针,则行为未定义。
初始化
由 new 表达式创建的对象将按照以下规则进行初始化。
如果 type 不是数组类型,则在获取的内存区域中构造单个对象:
|
(C++11 起) |
如果 type 是数组类型,则会初始化一个对象数组:
- 如果 new-initializer 缺失,每个元素将被 默认初始化 。
-
- 即使第一维度为零,仍需满足对假设元素进行默认初始化的语义约束。
- 如果 new-initializer 是一对圆括号,则每个元素都将进行 值初始化 。
-
- 即使第一维度为零,仍需满足对假设元素进行值初始化的语义约束。
|
(C++11 起) |
|
(C++20 起) |
初始化失败
如果初始化因抛出异常而终止(例如来自构造函数),程序将查找匹配的释放函数,随后:
- 如果能够找到合适的解分配函数,则调用该解分配函数来释放正在构造对象的内存。此后,异常会在 new 表达式上下文中继续传播。
- 如果无法找到明确匹配的解分配函数,则传播异常不会释放该对象的内存。这仅在被调用的分配函数未分配内存时适用,否则很可能导致内存泄漏。
匹配的释放函数的 查找 范围按以下方式确定:
-
如果
new
表达式不以
::开头,且分配的类型是类类型T或类类型T的数组,则会在T的类作用域中搜索解分配函数的名称。 -
否则,或者如果在
T的类作用域中未找到任何内容,则通过在 全局作用域 中搜索来查找解分配函数的名称。
对于非布置分配函数,使用常规的释放函数查找机制来匹配对应的释放函数(参见 delete 表达式 )。
对于布置分配函数,其匹配的释放函数必须具有相同数量的参数,且除第一个参数外,每个参数类型必须与分配函数对应的参数类型相同(在 参数转换 之后)。
- 如果查找找到一个匹配的释放函数,该函数将被调用;否则,不会调用任何释放函数。
- 如果查找找到一个非布置释放函数,且该函数作为布置释放函数时本应被选为与分配函数匹配,则程序非良构。
无论如何,匹配的释放函数(如果存在)必须 未被删除且 (since C++11) 在 new 表达式出现的位置可访问。
struct S { // 布局分配函数: static void* operator new(std::size_t, std::size_t); // 非布局释放函数: static void operator delete(void*, std::size_t); }; S* p = new (0) S; // 错误:非布局释放函数与布局分配函数匹配 // (函数签名冲突)
如果在 new 表达式中调用解分配函数(由于初始化失败),传递给该函数的参数按以下方式确定:
- 第一个参数是从分配函数调用返回的值(类型为 void * )。
- 其他参数(仅适用于布置释放函数)是传递给布置分配函数的 placement-args 。
如果实现允许在调用分配函数时引入临时对象或复制任何参数,那么是否在分配和释放函数的调用中使用同一对象是未指定的。
内存泄漏
由 new 表达式创建的对象(具有动态存储期的对象)会持续存在,直到 new 表达式返回的指针被用于匹配的 delete-expression 。如果指针的原始值丢失,该对象将变得不可访问且无法被释放:此时会发生 内存泄漏 。
在以下情况下指针可能被赋值:
int* p = new int(7); // 动态分配的整型变量,值为7 p = nullptr; // 内存泄漏
或者当指针超出作用域时:
void f() { int* p = new int(7); } // 内存泄漏
或由于异常:
void f() { int* p = new int(7); g(); // 可能抛出异常 delete p; // 无异常时正常执行 } // 若g()抛出异常则导致内存泄漏
为简化动态分配对象的管理, new 表达式的结果通常存储在 智能指针 中: std::auto_ptr (C++17前) std::unique_ptr 或 std::shared_ptr (C++11起) 。这些指针能确保在上述场景中正确执行delete表达式。
注释
Itanium C++ ABI 要求当所创建数组的元素类型具有平凡析构函数时,数组分配开销必须为零。MSVC 也遵循这一要求。
某些实现(例如 VS 2019 v16.7 之前的 MSVC)要求对非分配布置数组 new 使用非零数组分配开销(若元素类型不可平凡析构),该行为自 CWG issue 2382 起不再符合标准。
一种不分配内存的placement数组 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
需要
检查参数是否为空 |
空参数导致未定义行为 |
| 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
表达式中
推导数组边界 |
允许推导 |