C++ operator new重载与调用

最近研究godot源码时发现它重载了operator new函数,调用的形式也各式各样,很是困惑。所以测试总结了一下new的各个调用格式和对应意义。

C++的默认new运算符做了三件事。一,分配指定字节大小的堆空间;二,调用对应类的构造函数;三,返回对应类型的指针。我们不能重写这个默认的new运算符,但可以重写标准库提供的operator new,指定编译器用我们重写的版本替换默认的new运算符,从而达到各种各样的目的。

其实在实际的使用中,如果重写了operator new函数,你会发现各种各样的new运算调用形式,其中不乏部分反直觉的格式。大致如下

class Test;
{//某段代码块
	Test* t = (Test*)malloc(sizeof(Test));
	Test* t0 = new Test;
	Test* t1 = ::new Test;
	Test* t2 = (Test*)::operator new (sizeof(Test));
	Test* t3 = new (t) Test;
	Test* t4 = new ("ch",1) Test(1);
}

如果你对上面的五花八门的new表达式感到困惑,不明白各个表达式都是什么意思,指向的是那种形式的重载,那么你可以继续看下去。本文就是为了分析总结这些表达式分别是什么含义。

0、调用时的new与operator new

C++中的new关键字涉及到的表达式其实有两种形式,一种是以new关键字为唯一关键字的特殊语法,下面均称为new表达式。忽略与new本身无关的访问运算符,基本完整的new表达式的调用格式如下:

new (额外参数列表) 类型(构造参数)

而另一种就是作为标准的函数调用,如 t 2 \color{#FF0000}{t2} t2就是直接调用的一个operator new函数。其调用格式符合标准的函数调用语法。

operator new函数是我们可以自定义重载的部分,一般只完成内存空间的分配工作。以new为关键字做语法调用时(以下用new表达式指代),我们会首先调用参数匹配的operator new完成空间配置工作,然后根据类型以及初始化参数构造对象并返回相应类型的指针。

当我们使用new表达式动态申请内存时,
1、 表达式首先根据额外的参数寻找参数匹配的operator new函数,并传入表达式中类型的size(作为第一个参数)和额外参数,供operator new函数使用
2、operator new函数通常会申请内存,并返回一个void*类型的指针
3、new表达式根据表达式中的初始化列表初始化这块内存,并返回类型匹配的指针

int a = 1;
Test* t = new (a) Test;
//上面的表达式等价于
void* temp = operator new(sizeof(Test), a); // 匹配到参数为(size_t,int)的operator new函数,分配空间
*(Test*)temp = Test();	//初始化空间
Test* t = (Test*)temp;  //匹配类型返回指针

new表达式包含了对operator new函数的调用,反之operator new函数只是整个new表达式工作的一部分。

ps:本文的表述均基于cpp文档、测试以及个人猜想,实际运行过程可能与表述不符。仅为便于识别理解。我还真没见过用我这种说法描述new的实际运行的,很有可能是错的。

1、额外参数

因为operator new函数通常都会完成空间配置工作,所以也会被称为allocation functions
最常见的operator new函数函数声明如下

void* operator new(size_t size);

t0、t1(表达式调用函数)t2(直接调用函数) 都是调用到了形式为void* operator new(size_t size) 的常用operator new函数

operator new函数允许重载,且大部分允许用户重写覆盖标准库的版本(如果有同样形参的operator new函数的话),它们都遵循下面的声明标准:

void* operator new(size_t size[, Type1 arg1[, Type2 arg2...]]);

返回值必须是viod*。size是operator new函数中不可缺少的参数,且必须在第一位,表示欲申请的空间的大小。除了size以外,operator new函数还可以添加一些别的参数,这些参数就是额外参数。

当new表达式需要使用特殊的operator new函数重载时,可以在new关键字和类型名之间插入除了size以外的实参列表,就像 t3和t4 一样:
t3的new表达式匹配了一个形如void* operator new(size_t size,void* ptr) 的operator new函数;
t4则匹配了形参列表为(size_t size, const char* ch, int len)的operator new函数。

我们正常使用的new表达式和operator new函数是不包含额外参数的,带有这部分额外参数的operator new和其对应的new表达式被称为placement new

在通过new表达式调用时,因为后面显式地交代了想要存储的类型,我们在new关键字和类型名之间的实参列表中,省略了operator new函数的第一个参数size(用于指定申请的内存空间大小,单位字节,等于sizeof(T)),从第二个参数开始。

在operator new函数中,我们一般只会完成内存的申请工作(其他的工作如内存初始化以及指针类型的转换交给new表达式完成),基本流程如下:

if (void* ptr = malloc(size))
    return ptr;
else
    throw std::bad_alloc();

但也有例外,placement new中有一个特殊的存在( t 3 \color{#FF0000}{t3} t3 ):

void* operator new(size_t size,void* ptr) {
	return ptr;
}

这种形式的operator new不允许被重写,只能使用标准库提供的版本,其本身并不对传入的ptr指针做任何处理,直接返回,既不会分配空间,也不会初始化内存(new函数本身不负责调用构造函数,起作用的还是new表达式最后的类型)。你可以写成如下形式进行测试,其调用的确实是void* operator new(size_t size,void* ptr),但并没有调用构造函数。

// t要保证指向安全的内存空间,不论是栈或堆
t = (Test *)::operator new(sizeof(Test), t); // 并不会调用构造。只是测试,一般不会这样写

这个placement new常常被用来重新初始化已经分配的内存——通过new表达式的方式。标准用法如下:

// 这是标准用法,参考自cppreference
T* tptr = new(buf) T; // 构造一个 `T` 对象,将它直接置于
                      // 你预分配的位于内存地址 `buf` 的存储。

解析这个new表达式:首先调用了void* operator new(size_t size,void* ptr)版本的operator new。不同于其余版本的operator new,这个版本的new函数没有额外分配空间。然后构造了一个T对象,将其置于buf指向的内存。最后返回了T类型的指针。总的来看,这个表达式就好像完成了不分配内存但重新初始化内存的工作。

国内常常会用placement new特指这种形式的new表达式,将内存分配后的内存初始化的功能强加在这种形式的new表达式甚至是其对应的operator new函数上,虽然这个表达式确实完成了期望的工作,但实际过程完全不是一回事。

2、调用格式

new表达式以new为唯一关键字,这个表达式语句可以认为是一种独特的语法。

T* t = new T; //这是最简略的new表达式。在new表达式里类型与new关键字无法省略

在类型的前面我们可以加上额外的参数列表(除了第一个参数size),new表达式会根据参数匹配相应的operator new函数。在类型的后面我们可以加上构造列表,构造一个我们期望的初始对象存储于内存中。

T* t = new (.../*operator new 参数列表,省略第一个参数*/) T(... /*构造参数*/)

当你希望直接调用某个版本的operator new函数时,你需要完整的写出operator new关键字(如 t 2 \color{#FF0000}{t2} t2)来调用operator new函数,且后面没有类型(这个时候就是一个标准函数)。它会严格按函数定义执行(返回void*指针,你需要显示的转换成你希望的),大多数情况下只分配了内存但没有初始化。

void* r = operator new(size, ...); //这个时候你在调用一个普通的函数,在函数调用后加上类型是错误的语法。

所以如下的调用形式基本会报错:

// 以下都是错误做法,请不要过脑。
void* t = new (sizeof(T), ...); 
T* t = (T*)new (sizeof(T), ...); 
T* t = operator new(...) T; 

一个new表达式,最基本的会完成初始化与返回匹配类型的指针的职能,是否分配新空间取决于对应的operator new函数。你可能希望将内存的申请分配与内存的初始化两件事分开来,如果一定希望借由new表达式和new函数完成,可以按下面的写法:

// 正确的做法 
T* t = operator new(sizeof(T)); // 这样只分配不初始化。
t = new (t) T(...);

// 能正常运行但不是预期的步骤,下面两句调用了两次构造
T* t = new T; 
t = new (t) T(...);
3、作用域

operator new可以重载在全局作用域或者类内部,当类内部存在参数匹配的operator new 函数实现时,会优先调用类内部的operator new,否则会在全局作用域内寻找。如果都寻找无果,则会调用标准版本。

可以使用访问运算符强制使用你期望的版本,比如 t 1 \color{#FF0000}{t1} t1中我们就强制指定new表达式使用全局作用域中的重载版本。

4、总结

几个名词 \color{#FF0000}几个名词 几个名词
placement new——指带有除了size参数以外其他参数的operator new函数和其对应的new表达式
不配置空间的placement new——特指void* operator new(size_t size,void* ptr),其不能被重载,函数本身不对传入的指针做任何处理。
operator new函数——形式为返回void*,参数最少带有一个size_t的new函数
new函数与new表达式
——这部分并非标准定义,只是个人为了区分内容做的个人定义,针对调用过程而言。
——作为new函数调用指的是在表达式中使用“operator new”两个关键字的格式调用,参数列表、返回值严格按照new 函数声明执行,调用方法与普通函数相同(无法再最后加上类型)。作为new 函数调用时,表达式只完成基本的空间配置工作(除了不配置空间的placement new),不会做指针的转换与内存的初始化工作
——作为new表达式调用在本文特指只使用“new”关键字的表达式语法,必须包含类型,省略size_t参数。new表达式完成完整的内存分配(交给new函数)、内存初始化(交给类型)、指定类型指针返回工作。

t 0 − t 4 的表达式解析 \color{#FF0000}t0-t4的表达式解析 t0t4的表达式解析

表达式作为new表达式还是new函数调用完成工作备注
t0new表达式内存分配;内存初始化;返回指针
t1new表达式内存分配;内存初始化;返回指针调用全局版本的自定义的operator new函数
t2new函数返回指针
t3new表达式内存初始化;返回指针不配置空间的placement new
t4new表达式内存分配;内存初始化;返回指针标准的placement new,可以通过额外参数完成特殊工作
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值