2023-08-18
C++
首先应该明白的是,new
和 operator new
最大的区别在于:
-
new
是一个 C++ 语法关键字; -
operator new
是 C++ 标准库中的一个函数,其函数签名是:// 定义在 <new> 头文件中 void* operator new( std::size_t count );
new 关键字会调用 operator new 函数
当你使用 new
这个关键字(即 new
表达式)去“创建对象”的时候,它会进行如下两个步骤:
- 首先,调用
operator new
函数来申请一块内存空间,大小就是正在创建的对象的大小; - 然后,如果创建的是一个类对象的话,就在这块内存空间上调用这个类的构造函数,对内存空间进行初始化。
用代码来说明上述过程:
Object* p = new Object(value);
等价于:
void* v = operator new(sizeof(Object));
p = reinterpret_cast<Object*>(v);
p->Object::Object(value); // 这句话不是合法的 C++ 代码,因为 C++ 不允许我们直接
// 调用构造函数,这里只是为了展示编译后的代码行为。
也就是说,编译器会把上面的代码编译成机器码,而这个机器码在逻辑上等价于下面的代码。
而 delete
表达式与 operator delete
函数之间的关系也一样:
delete p;
等价于:
p->~Object();
operator delete(p);
new[]
/ delete[]
表达式也一样,会调用 operator new[]
/ operator delete[]
这个函数来分配连续的空间。
Placement new 的情况
我们知道,new
表达式有一个常见的变体是 Placement new,即你可以额外传一个地址参数给它:
Object* obj = new(ptr) Object(); // 在 ptr 处构造对象,没有额外申请内存空间。
此时,Placement new 表达式会调用 operator new
函数的一个重载版本:
void* operator new( std::size_t count, void* ptr );
这个重载版本拥有第二个参数 ptr
,这个 ptr
就是你在 Placement new 表达式里传的那个参数。
这个 Placement operator new
函数的实现非常简单,就是直接将 ptr
参数返回:
void* operator new( std::size_t count, void* ptr ) {
return ptr;
}
这就让后续构造过程在 ptr
地址处发生,达到 Placement new 的效果。
::new 和 ::operator new
你会经常见到别人写 new
或 operator new
的时候额外加上 ::
(scope resolution operator),即 ::new
和 ::operator new
。这是为什么呢?
C++ 允许我们通过操作符重载,为特定 class 实现特定的 operator new
。例如下面的代码为 X
这个类型定义了 operator new
和 operator new[]
,让它们在被调用时打印一条信息:
#include <iostream>
// class-specific allocation functions
struct X
{
static void* operator new(std::size_t count)
{
std::cout << "custom new for size " << count << '\n';
return ::operator new(count);
}
static void* operator new[](std::size_t count)
{
std::cout << "custom new[] for size " << count << '\n';
return ::operator new[](count);
}
};
int main()
{
X* p1 = new X;
delete p1;
X* p2 = new X[10];
delete[] p2;
}
输出:
custom new for size 1
custom new[] for size 10
即,new
表达式会因为 X
拥有自定义的 operator new
和 operator new[]
而去调用它们。
所以,如果我们希望绕过一个类型自己定义的 operator new
或 operator new[]
,就需要在 new
关键字前加一个 ::
,代表使用 global namespace 下的那个标准库定义的 operator new
函数。当然,如果你只想调用这个标准库定义的 operator new
函数用于空间分配,也可以直接调用 ::operator new
函数,不使用 ::new
表达式。
new 的更多用法:控制内存对齐和抛出异常
前面提到的 Placement new 表达式,即 new(param)
,其实只是 new
能额外传参数的一种表现。如果你去看标准库中 operator new
函数的所有重载版本,还可以见到以下两种版本:
void* operator new( std::size_t count, std::align_val_t al );
void* operator new( std::size_t count, const std::nothrow_t& tag );
第一种版本用于强制要求分配的空间具有更严格的内存对齐,对齐为 al
的整数倍,如:
int* p = new(std::align_val_t{4096}) int; // 分配 4096 字节对齐的空间
这在某些应用场景下非常有用,如 Metal 要求如果要从一块儿已有内存空间创建 MTLBuffer
,就需要这块儿内存空间是 page-size 对齐的。
第二种版本,则是可以要求这次 new
一定不抛出异常,如果内存申请失败,则返回空指针:
int* p = new(std::nothrow) int[100000000ul]; // non-throwing overload
if (p == nullptr) {
std::cout << "Allocation returned nullptr\n";
}
当然,这也满足了某些要求不能抛出异常的应用场景。
总结
一开始仅仅想了解 new
和 operator new
的区别,之后顺藤摸瓜找到许多关于 new
的新知识。所以,之后再遇到 new
表达式的各种用法时,就不再惧怕,因为它们本质上都是在调用 operator new
函数的各种重载版本而已,new
只是起到一个类似语法糖的作用,并没有引入新的内容。