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



    一、new is an expression

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

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

    这种我们经常使用的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
    • 2
    • 3
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在类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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

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

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

    实用:

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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


  • 相关阅读:
    【剑指offer系列最终篇-END】75. 树中两个结点的最低公共祖先
    操作系统存储管理
    10min快速回顾C++语法(三)
    Django(九、choices参数的使用、多对多表的三种创建方式、Ajax技术)
    Python爬虫之Js逆向案例(13)-某乎最新x-zse-96的rpc方案后续
    电脑重装系统 win11 怎么关闭系统软件通知
    羧基修饰的聚苯乙烯微球(红色、橙色、绿色)的产品简介
    Windows安装nginx
    机器人制作开源方案 | 双轮提升搬运小车
    HTML+CSS大作业:众志成城 抗击疫情 抗击疫情网页制作作业 疫情防控网页设计
  • 原文地址:https://blog.csdn.net/zsiming/article/details/125504375