C++ 中 new 和 operator new 的区别与联系

2023-08-18 C++

首先应该明白的是,newoperator new 最大的区别在于:

new 关键字会调用 operator new 函数

当你使用 new 这个关键字(即 new 表达式)去“创建对象”的时候,它会进行如下两个步骤:

  1. 首先,调用 operator new 函数来申请一块内存空间,大小就是正在创建的对象的大小;
  2. 然后,如果创建的是一个类对象的话,就在这块内存空间上调用这个类的构造函数,对内存空间进行初始化。

用代码来说明上述过程:

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

你会经常见到别人写 newoperator new 的时候额外加上 ::scope resolution operator),即 ::new::operator new。这是为什么呢?

C++ 允许我们通过操作符重载,为特定 class 实现特定的 operator new。例如下面的代码为 X 这个类型定义了 operator newoperator 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 newoperator new[] 而去调用它们。

所以,如果我们希望绕过一个类型自己定义的 operator newoperator 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";
}

当然,这也满足了某些要求不能抛出异常的应用场景。

总结

一开始仅仅想了解 newoperator new 的区别,之后顺藤摸瓜找到许多关于 new 的新知识。所以,之后再遇到 new 表达式的各种用法时,就不再惧怕,因为它们本质上都是在调用 operator new 函数的各种重载版本而已,new 只是起到一个类似语法糖的作用,并没有引入新的内容。

参考