new、express new、operator new、placement new 之间的千丝万缕

本文详细介绍了C++中new表达式的内部机制,包括operatornew的重载以及placementnew的使用。通过示例代码展示了如何自定义内存分配方式,并解释了placementnew如何在已分配内存上执行构造函数。此外,还讨论了placementnew在内存不足时的nothrow特性。


一、new is an expression

在C++中,new经常被用来进行动态内存的分配,例如:

Foo* obj = new Foo; //其中Foo是一个类

这种我们经常使用的new其实在C++中称为“expression”。一个expression是没办法重载的。所以,我们没办法从 expression new入手来定义一个我们想要的版本。但是,expression new 经过编译器之后执行的动作却让我们有机可趁。在一个new expression 之后,编译器其实做了三件事:

1. void* men = operator new(sizeof(Foo));
2. Foo* ptr = static_cast<Foo*>(men);
3. ptr->Foo("waterMelon", 30)
  1. 第一,编译器帮我们调用了operator new 来 申请一块干净的内存(未经过初始化)。
  2. 第二,编译器帮我们把得到那块内存用static_cast函数进行类型转换成对应的类型。(C++的四种cast动作详解
  3. 第三,编译器帮我们调用了构造函数,在分配的内存中进行对象的构造。

在这个过程中,让我们有机可趁的地方是步骤一中的 operator new 函数,因为它是一个可以被进行重载的函数。 在步骤一中 operator new 接收了一个参数,指明了“我想要多少字节的内存”。而对于后续的步骤二、三来说,从步骤一中得到的内存究竟是从哪儿来的其实它们并不关心,毕竟它们的职责只是构造对象而已。因此,我们就能对某些版本的operator new进行重载,来进行内存管理或者做其他的事情。


二、operator new

为了验证上面的事情(使用expression之后,编译器做了三件事),我们可以使用下面的程序进行测试:

#include <iostream>
#include <cstdlib>
#include <string>

using namespace std;

class Foo {
public:
	Foo() = default;
	Foo(string name, int value) : _name(name), _value(value) {}
	void showContent() 
	{ cout << "name:" << _name << " " << "value:" << _value << endl; }
	
	void* operator new(size_t size); //重载operator new 操作

private:
	string _name;
	int _value;
};

void* Foo::operator new(size_t size) {
	cout << "call my operator new" << endl;
	return malloc(size);
}

int main() {
	Foo* obj = new Foo("waterMelon", 30);
	obj->showContent();

	delete obj;

	return 0;
}

在类Foo 中,我们定义了自己的operator new 版本。因此,在主程序中,一旦使用了expression new的操作,编译器就自动帮我们做三件事,而第一件事就是调用类作用域内重载的operator new,因此,在控制台会对结果进行输出:

在这里插入图片描述
可以看见,确实new expression 确实调用了我们自己重载的new operator版本。然而,对于后续的转型、构造来说,它们并不关心这块内存究竟是哪儿来的。在上面的例子中,这块内存其实只是简单的调用C中的malloc,而在我们使用的STL容器中,这块内存其实是从内存池(由allocator类维护)挖出来的,每次需要的时候,就从自己维护的内存池中分配一块出来返回,进行后续的转型和初始化。这是STL分配器中进行内存管理的方法,为的是减少不必要的空间浪费(cookie)和减少malloc调用。当然,这逐渐偏离了现在写的主题

总的来说,operator new 其实就是给了我们一个机会来进行函数的重载,让我们有办法去“什么地方”拿一块内存而已。

三、placement new(一)

上面讲到了使用expression new之后发生的三件事以及我们如何使用operator new 来抓住“内存分配”这个动作进行重载,得到我们想要的版本。对于步骤二、步骤三是编译器帮我们做的事情。现在的问题是,编译器做的事情,你自己能做吗?
假设我从哪儿拿到了一块干净的内存,我能不能自己转型、然后调用构造函数来构造对象?以下为测试程序:

#include <iostream>
#include <cstdlib>
#include <string>

using namespace std;


class Foo {
public:
	Foo() = default;
	Foo(string name, int value) : _name(name), _value(value) {}
	void showContent() 
	{ cout << "name:" << _name << " " << "value:" << _value << endl; }
	
	void* operator new(size_t size); //重载operator new 操作

private:
	string _name;
	int _value;
};

void* Foo::operator new(size_t size) {
	cout << "call my operator new" << endl;
	return malloc(size);
}

int main() {
	Foo* obj = new Foo("waterMelon", 30);
	obj->showContent();
	delete obj;
	
	// 尝试在干净的内存中调用构造函数
	void* men = malloc(sizeof(Foo));
	Foo* ptr = static_cast<Foo*>(men);
	ptr->Foo("waterMelon", 30);
	
	return 0;
}

结果如下:
在这里插入图片描述
看来我们有点异想天开了,编译器根本不允许我们自己来“转型+构造”。那有没有可能C++也提供了什么函数来完成这两个步骤?答案已经呼之欲出了,那就是使用placement new。placement new 允许我们在一块已经分配的内存中来进行构造函数的调用。注意,下面是placement new 的用法:

// 注意,下面 place_address 是某块用来构造对象的内存(指针),type是某个类的名称
new (place_address) type  // 默认构造
new (place_address) type (initializers) // 传入参数,见下面的例子

实用:

#include <iostream>
#include <cstdlib>
#include <string>
#include <new>

using namespace std;


class Foo {
public:
	Foo() = default;
	Foo(string name, int value) : _name(name), _value(value) {}
	void showContent() 
	{ cout << "name:" << _name << " " << "value:" << _value << endl; }
	
	void* operator new(size_t size); //重载operator new 操作

private:
	string _name;
	int _value;
};

void* Foo::operator new(size_t size) {
	cout << "call my operator new" << endl;
	return malloc(size);
}

int main() {

	void* men = malloc(sizeof(Foo));
	Foo* obj2 = ::new(men) Foo("banana", 20); // 使用placement new进行对象的构造
	obj2->showContent();
	delete obj2;

	return 0;
}

执行结果:
在这里插入图片描述
从上面可以总结得到,placement new就是标准库开放给我们在一块已经分配的内存中来进行构造函数的调用的接口而已。再次注意:由于placement new动作是“构造”而不是“分配”,因此,没有对应的placement delete。而上面的new、operator new 都涉及了内存的分配,因此有对应的delete、operator delete(可重载)。


四、placement new(二)

在placement new(一)中我们从new、express new、operator new、placement new出发讲述了关于placement new在整个故事中解决的问题和用法。在实际应用中,placement new还有一种用法:placement new 是可以用来分配内存的。相比于直接使用 expression new, placement new可以接受额外的参数。比如,在分配内存的过程中,计算机没有足够的内存可以分配了,那么相比于expression new, placement new可以接受额外的的参数 nothrow来禁止在没有足够内存的情况下抛出异常:

int* p1 = new int; //分配失败则抛出异常
int* p2 = new(nothrow) int; //分配失败只会返回一个空指针

在实现上,placement new 其实调用的是operator new。实际上,placement new可以很多形式,括号内的参数也可以有很多种。不同的形式其实只是对应了不同的operator new的重载版本而已。

比如上面的例子中,

new(nothrow) 
调用
operator new(size_t size, nothrow_t&noexcept;

而在placement new(一)中,

::new(men) Foo("banana", 20)
调用
operator new(size_t size, void*);

注意参数 size_t size 是编译器来传值的,所以在调用的时候只需要传一个参数就好。那这里为什么要使用全局的placement new来调用呢?原因就是这个版本的operator new是不允许用户进行重载的:

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

总结:

至此,所有的故事已经讲完,关于它们之间的关系,以下图一图概之:

![在这里插入图片描述](https://img-blog.csdnimg.cn/45fd3cae611d49cf82b5afdebc7c4912.png

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值