命名空间
变体
操作

new 表达式

来自 cppreference.com
< cpp‎ | 语言
 
 
C++ 语言
 
 

创建并初始化具有动态存储期的对象,即其生存期不一定受限于创建它们的作用域的对象。

内容

[编辑] 语法

::(可选) new (类型 ) new 初始化器 (可选) (1)
::(可选) new 类型 new 初始化器 (可选) (2)
::(可选) new (放置参数 ) (类型 ) new 初始化器 (可选) (3)
::(可选) new (放置参数 ) 类型 new 初始化器 (可选) (4)
1,2) 尝试创建由类型标识符 类型表示的类型的对象,该类型可以是数组类型,并且可以包含占位符类型说明符(C++11起),或者包含要通过类模板参数推导推导出其参数的类模板名称(C++17起)
3,4)(1,2) 相同,但是为分配函数提供了额外的参数,请参阅放置 new

[编辑] 说明

类型 - 目标类型标识符
new 初始化器 - 括号括起来的表达式列表花括号括起来的初始化列表(C++11起)
放置参数 - 额外的放置参数


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 初始化器 不是可选的

  • 类型 中使用了占位符,即 auto decltype(auto)(C++14起),可能与类型约束组合使用(C++20起)
(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)

在以下情况下,指定第一个维度的表达式的值无效

  • 该表达式为非类类型,并且其在转换为 std::size_t 之前的值为负数;
  • 该表达式为类类型,并且其在用户定义的转换函数之后和第二次标准转换之前的值为负数;
  • 表达式的值大于某些实现定义的限制;
  • 该值小于花括号括起来的初始化列表中提供的数组元素数量(包括字符串字面量上的终止符 '\0')。

如果由于任何这些原因,第一个维度中的值无效,

(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 表达式分配 charunsigned char, 或 std::byte(自 C++17 起) 数组,则如果稍后将一个对象放入已分配的数组中,它可能会根据需要从分配函数请求额外的内存,以保证所有类型不超过请求数组大小的对象的正确对齐。

new 表达式允许省略或组合通过可替换分配函数进行的分配。在省略的情况下,编译器可以提供存储空间,而无需调用分配函数(这也允许优化掉未使用的 new 表达式)。在组合的情况下,如果满足以下所有条件,则可以扩展由 new 表达式 E1 进行的分配,以便为另一个 new 表达式 E2 提供额外的存储空间:

1)E1 分配的对象的生命周期严格包含由 E2 分配的对象的生命周期。
2) E1E2 将调用相同的可替换全局分配函数。
3) 对于抛出异常的分配函数,E1E2 中的异常将首先在同一个处理程序中捕获。

请注意,仅当使用 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 表达式会立即返回,它不会尝试初始化对象或调用释放函数。如果将空指针作为参数传递给非分配放置 new 表达式,这将导致所选的标准非分配放置分配函数返回空指针,则行为未定义。

[编辑] 初始化

new 表达式创建的对象将根据以下规则进行初始化。

如果 type 不是数组类型,则在获取的内存区域中构造单个对象

  • 如果缺少 new-initializer,则对象将进行默认初始化
  • 如果 new-initializer 是括号括起来的表达式列表,则对象将进行直接初始化
  • 如果 new-initializer 是花括号括起来的初始化列表,则对象将进行列表初始化
(C++11起)

如果 type 是数组类型,则初始化对象数组

  • 如果缺少 new-initializer,则每个元素都将进行默认初始化
  • 即使第一个维度为零,也需要满足默认初始化假设元素的语义约束。
  • 如果 new-initializer 是一对括号,则每个元素都将进行值初始化
  • 即使第一个维度为零,也需要满足值初始化假设元素的语义约束。
  • 如果 new-initializer 是花括号括起来的初始化列表,则数组将进行聚合初始化
(C++11起)
  • 如果 new-initializer 是括号括起来的非空表达式列表,则数组将进行聚合初始化
(自 C++20 起)

[编辑] 初始化失败

如果初始化因抛出异常而终止(例如,来自构造函数),程序将查找匹配的释放函数,然后

  • 如果可以找到合适的释放函数,则调用该函数以释放正在构造对象的内存。之后,异常继续在 new 表达式的上下文中传播。
  • 如果找不到明确匹配的释放函数,则传播异常不会导致释放对象的内存。只有在调用的分配函数不分配内存时才适用,否则可能会导致内存泄漏。

匹配释放函数的查找范围的确定如下

  • 如果 new 表达式不是以 :: 开头,并且分配的类型是类类型 T 或类类型 T 的数组,则在 T 的类范围内搜索释放函数的名称。
  • 否则,或者如果在 T 的类范围内找不到任何内容,则通过在全局范围内搜索释放函数的名称来查找它。

对于非放置分配函数,使用正常的释放函数查找来查找匹配的释放函数(请参阅delete 表达式)。

对于放置分配函数,匹配的释放函数必须具有相同的参数数量,并且除第一个参数之外的每个参数类型都与分配函数的对应参数类型相同(在参数转换之后)。

  • 如果查找找到单个匹配的释放函数,则将调用该函数;否则,将不调用任何释放函数。
  • 如果查找找到一个非放置释放函数,并且该函数(被视为放置释放函数)将被选为分配函数的匹配项,则程序是格式错误的。

在任何情况下,匹配的释放函数(如果有)都必须 未被删除并且(自 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-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_ptrstd::shared_ptr(自 C++11 起)。这些指针保证在上述情况下执行 delete 表达式。

[编辑] 注释

Itanium C++ ABI 要求如果创建的数组的元素类型是可平凡销毁的,则数组分配开销为零。MSVC 也是如此。

一些实现(例如 VS 2019 v16.7 之前的 MSVC)要求,如果元素类型不是可平凡销毁的,则在非分配位置数组 new 上需要非零数组分配开销,这在 CWG 问题 2382 之后不再符合标准。

创建一个 unsigned char, 或 std::byte(自 C++17 起) 数组的非分配位置数组 new 表达式可用于在给定存储区域上隐式创建对象:它结束与数组重叠的对象的生命周期,然后在数组中隐式创建隐式生命周期类型的对象。

std::vector 为一维动态数组提供了类似的功能。

[编辑] 关键字

new

[编辑] 缺陷报告

以下行为更改缺陷报告已追溯应用于先前发布的 C++ 标准。

DR 应用于 发布时的行为 正确行为
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 表达式中无法推导出数组边界
允许推导

[编辑] 另请参阅