• 深入了解C++中各种不同意义的new和delete


    前言

    有时候我们觉得,C++术语仿佛是要故意让人难以理解似的。这里就有一个例子:请说明new operator 和 operator new 之间的差异。
    上面这段话出自《More Effective C++》中的条款8。有兴趣的读者可以阅读这本书。现在就让我们揭开这神秘的面纱吧。

    new 到底做了什么

    new是C++的一个关键字、操作符。
    当我们执行Test* pt = new Test();这句代码时,实际上干了三件事情:

    1. 分配内存
    2. 调用Constructor函数
    3. 返回分配好的指针

    为什么这么说呢?口说无凭眼见为实,请接着往下看。

    通过VS2022查看汇编代码进行验证

    首先我们需要写一个空类,然后在main中new出这个类。代码可参考如下:

    class A
    {
    public:
    	A()
    	{
    		
    	}
    	~A()
    	{
    		
    	}
    };
    
    int main()
    {
    	A* p = new A();
    
    
    	delete p;
    	p = nullptr;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    第一步:在创建这一行添加断点(可左击该行行首或者在该行按F9即可)。
    第二步:开始调试到当前断点处(可按F5)。
    第三步:在上方功能栏中点击【Debug】->【Windows】->【Disassembly】。中文对应的是【调试】->【窗口】->【反汇编】。详细请看下图。
    在这里插入图片描述
    操作完上面三步之后我们就到了汇编代码。由于重点不是研究汇编语言,所以这里我就仅对上面那三步进行标记。验证一下上面的一个猜想。
    在这里插入图片描述

    那么这里我们用到的new操作符,也就是new operator,在《C++ Primer》书中也被称为new expression

    operator new

    功能:只负责内存分配
    operator new默认情况下调用分配内存的代码,去尝试在堆区获取一段空间,如果成功就返回,如果失败,则调用new_hander。有关new_hander我之前写了一篇:new_hander文章链接

    重载类内operator new

    下面对operator new重载,进行测试;

    class A
    {
    public:
    	A()
    	{
    		std::cout << "Call A Constructor!" << std::endl;
    	}
    	~A()
    	{
    		std::cout << "Call A Destructor!" << std::endl;
    	}
    
    	void* operator new(size_t size)
    	{
    		std::cout << "Call operator new" << "\t size = " << size << std::endl;
    		return ::operator new(size); // 通过::operator new调用了全局的new
    	}
    
    };
    int main()
    {
    	A* pt = new A();
    
    	delete pt;
    	pt = nullptr;
    
    	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

    运行结果:
    在这里插入图片描述
    可以看到先打印类内的operator new再调用constructor函数最后调用destructor函数。

    重载全局 ::operator new

    若要重载全局的::operator new时,最后就不能return 自身了需要写成malloc(size)。对应的delete也有delete operatoroperator delete俩种,operator delete也是可以重载的。所以一般来说重载了operator new 就需要重载对应的operator delete了。
    具体请看下面的代码:

    新增一个全局的operator new函数

    void* operator new(size_t size)
    {
    	std::cout << "Call global operator new" << "\t" << size << std::endl;
    	return malloc(size);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行结果:
    在这里插入图片描述

    直接调用operator new

    该函数我们可以进行重载,但是第一参数的类型必须是size_t。而且我们还可以单独调用operator new。将返回一个void类型的指针。

    在原有代码基础上,增加一个成员函数用于输出日志。

    class A
    {
    public:
    	A()
    	{
    		std::cout << "Call A Constructor!" << std::endl;
    	}
    	~A()
    	{
    		std::cout << "Call A Destructor!" << std::endl;
    	}
    
    	void* operator new(size_t size)
    	{
    		std::cout << "Call operator new" << "\t size = " << size << std::endl;
    		return ::operator new(size); // 通过::operator new调用了全局的new
    	}
    	void print()
    	{
    		std::cout << "ha ha !" << std::endl;
    	}
    };
    
    void* operator new(size_t size)
    {
    	std::cout << "Call global operator new" << "\t size = " << size << std::endl;
    	return malloc(size);
    }
    
    int main()
    {
    	void* rawMemory = operator new(sizeof(A));
    
    	A* pa = static_cast<A*>(rawMemory);
    	pa->print();
    
    	delete pa;
    	pa = nullptr;
    	
    	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
    • 39
    • 40
    • 41

    运行结果:
    在这里插入图片描述
    可以看到只打印了全局的operator new函数已经析构函数。

    Placement new

    头文件:#include 或者#include
    可以直接调用constructor函数,是operator new的一个特殊版本,也被称为placement new函数。

    需要实现一个void* operator new(size_t, void* location)的重载版本。不需要申请内存只需要返回当前对象即可。
    调用的语法:new(ObjectName) ClassName(构造函数的参数)

    class A
    {
    public:
    	A()
    	{
    		std::cout << "Call A Constructor!" << std::endl;
    	}
    	~A()
    	{
    		std::cout << "Call A Destructor!" << std::endl;
    	}
    
    	void* operator new(size_t size)
    	{
    		std::cout << "Call operator new" << "\t size = " << size << std::endl;
    		return ::operator new(size); // 通过::operator new调用了全局的new
    	}
    
    	void* operator new(size_t size, void* location)
    	{
    		std::cout << "Call operator new(size_t size, void* location)" << std::endl;
    		return location;
    	}
    	void print()
    	{
    		std::cout << "ha ha !" << std::endl;
    	}
    };
    
    int main()
    {
    	void* rawMemory = operator new(sizeof(A));
    
    	A* pa = static_cast<A*>(rawMemory); // 创建内存
    
    	new(pa) A(); // 调用构造函数
    
    	pa->print();
    
    	delete pa;
    	pa = nullptr;
    
    	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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    运行结果:
    在这里插入图片描述
    这里的operator new的目的是要为对象找内存,然后返回一个指针指向它。在placement new的情况下,调用者已经知道指向内存的指针了,所以placement new唯一需要做的就是将已获得指针进行返回。虽然说size_t参数没有用到但是必须要加,之所以不给形参名是因为防止编译器抱怨“某某变量未被使用”。

    删除与内存释放

    为了避免内存泄漏,每一个动态分配都必须匹配一个释放动作。
    内存释放的动作是由operator delete执行,函数原型:void operator delete(void* object);

    当我们写了这句代码时delete pa;实际上执行了俩件事。
    1、调用destructor函数
    2、释放对象所占的内存资源

    转换成代码就相当于:

    	pa->~A();
    	operator delete(pa);
    
    • 1
    • 2

    使用operator new创建对象该如何释放

    当我们在创建对象时,没有调用constructor函数,那么释放内存时也不需要调用destructor函数。只需要operator delete(pa);

    int main()
    {
    	void* rawMemory = operator new(sizeof(A));
    	
    .	...其他代码	
    
    	operator delete(rawMemory);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面这段代码其实就等价于C语言里面调用malloc和free函数。

    使用placement new创建对象时该如何释放

    如果使用placement new在内存中产生对象,我们不能使用delete operator,因为会调用operator delete函数来释放内存。首先该内存并不是由该对象的operator new函数分配而来。它仅仅做了一个返回而已,所以这种情况下只需要调用destructor函数即可。

    int main()
    {
    	void* rawMemory = operator new(sizeof(A));
    	
    
    	A* pa = static_cast<A*>(rawMemory); // 创建内存
    
    	new(pa)A(); // 调用构造函数
    
    	pa->~A();
    	pa = nullptr;
    
    	operator delete(rawMemory);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在上面这段代码中,pa对象就是使用placement new,所以最后只需要调用destructor函数。

    针对数组的创建和释放

    当我们使用A* pa = new A[10];这段代码时,分配内存的方式将会发生变化。
    1、由operator new 改为 operator new[],也被叫为array new。同样array new也可以被重载,
    2、array new必须调用数组中的每个对象的constructor函数。上面那个例子就会调用10个A的无参构造函数
    3、array new在释放内存时。上面那个例子就会调用10个A的destructor函数。
    4、该类必须有无参构造函数。

    所以我们同样也可以修改operator new[]所调用的 new operator函数,以及delete[] operator。

    系统维护开销

    在面对数组时,new 会额外分配空间来存储new的长度(一般为一个指针大小,32位平台下4字节,64位平台下8字节)。这个叫系统维护开销。
    下面是测试代码,类A是个空类只占一个字节,正常来说应该申请10个字节的内存。

    int main()
    {
    	A* pa = new A[10];
    
    
    	delete[] pa;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    32位环境下:
    在这里插入图片描述
    64位环境下:
    在这里插入图片描述
    可以看到对申请了一个指针的内存用来存放申请对象的个数。

    总结

    下面针对new的三种使用方式做了一个使用场景总结切记操作对应的new 时还需要对应的delete。
    1、需要将对象创建在堆区,那么就使用 new operator 也就是new操作符。它会帮你分配内存并调用constructor函数。
    2、仅需要分配内存,那么就使用operator new,这样就不会调用constructor函数。
    3、需要在堆区创建对象时自定义内存分配方式,那么就需要重写operator new函数然后使用new operator即可。
    4、需要在已分配的内存中调用构造函数,那么就使用placement new

  • 相关阅读:
    算法刷题介绍
    C#switch…case…判断
    基于JAVA清颜广告股份有限公司网站演示录像计算机毕业设计源码+数据库+lw文档+系统+部署
    【算法刷题day32】Leetcode:122. 买卖股票的最佳时机 II、55. 跳跃游戏、45. 跳跃游戏 II
    带你秒懂二叉树旋转
    inet_ntop4源代码实现
    使用pro-components遇到的问题
    SwiftUI 4.0 中原生图表(Charts)实现超长内容滚动功能
    人体神经系统结构图高清,人体神经系统全貌图片
    压缩状态DP位运算
  • 原文地址:https://blog.csdn.net/qq_45254369/article/details/126276038