浅谈C++中各种不同意义的new和delete的使用

目录
  • 前言
  • new 到底做了什么
    • 通过VS2022查看汇编代码进行验证
  • operator new
    • 重载类内operator new
    • 重载全局 ::operator new
    • 直接调用operator new
  • Placement new
  • 删除与内存释放
    • 使用operator new创建对象该如何释放
    • 使用placement new创建对象时该如何释放
  • 针对数组的创建和释放
    • 系统维护开销
  • 总结

前言

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

new 到底做了什么

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

  • 分配内存
  • 调用Constructor函数
  • 返回分配好的指针

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

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

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

class A
{
public:
	A()
	{

	}
	~A()
	{

	}
};

int main()
{
	A* p = new A();

	delete p;
	p = nullptr;
	return 0;
}

第一步:在创建这一行添加断点(可左击该行行首或者在该行按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;
}

运行结果:

可以看到先打印类内的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);
}

运行结果:

直接调用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;
}

运行结果:

可以看到只打印了全局的operator new函数已经析构函数。

Placement new

头文件:#include <new> 或者#include <new.h>
可以直接调用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;
}

运行结果:

这里的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);

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

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

int main()
{
	void* rawMemory = operator new(sizeof(A));

.	...其他代码	

	operator delete(rawMemory);
	return 0;
}

上面这段代码其实就等价于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;
}

在上面这段代码中,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;
}

32位环境下:

64位环境下:

可以看到对申请了一个指针的内存用来存放申请对象的个数。

总结

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

到此这篇关于浅谈C++中各种不同意义的new和delete的使用的文章就介绍到这了,更多相关C++ new和delete 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++中new和delete的使用方法详解

    C++中new和delete的使用方法详解 new和delete运算符用于动态分配和撤销内存的运算符 new用法:           1.     开辟单变量地址空间 1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5           2.     开辟数组空间 一维: int *a = new in

  • C++ 动态内存分配详解(new/new[]和delete/delete[])

    一.为什么需要动态内存分配? 在C++程序中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的. 但是可能存在程序的内存需求只能在运行时确定的情况. 例如,当需要的内存取决于用户输入. 在这些情况下,程序需要动态分配内存,C ++语言将运算符new和delete合成在一起. (1)特点 1.C++中通过new关键字进行动态内存申请 2.C++中的动态内存分配是基于类型进行的 3.delete关键字用于内存释放 (2)语法 ①变量申请: Type* pointer = new Type;

  •  C++ new 和 delete 关键字详解

    目录 前言 new 和 delete 的使用 new delete 为数组分配内存和释放内存 malloc 和 new delete 和 delete[] 前言 最早接触到new这个关键字,是在 Java 中,然后 ES6 之后的 js 中也提供了 new 这个关键字,在 java 和 js 这两门语言中,使用 new 关键词可以实例化类的对象.语义是相似的,但是其背后还是有些差异的,js 的 new 和 class 可能是一些语法糖. 那么有了这些对 new 这个关键字用法的了解,我们今天来看

  • C++超详细探究new/delete的使用

    目录 内存管理 new/delete 1.new运算符的使用 2.new的函数方法的使用 3.定位new new/delete/malloc/free区别 内存管理 在C++中,一个可执行程序的虚拟地址空间可分为,内核.栈.共享库的内存映射区域.堆.数据区和代码段,具体分布额如下图所示: 内核: 操作系统 栈区: 函数的形参,非静态的局部变量,函数现场保护数据等等,栈是向下增长的. 共享库的内存映射区域 用于装载一个共享的动态内存库.用户可使用系统接口创建共享内存,做进程间通信. 堆区: 用于程

  • C++中new和delete的介绍

    介绍 1.malloc,free和new,delete区别. a.malloc,free是C/C++的标准库函数.new,delete是c++的操作符. b.malloc申请的是内存,严格意义不是"对象",new申请的可以理解为"对象",new 时会调用构造函数,返回指向该对象的指针. c.对于class类型,必须用new/delete来创建和销毁,自动调用构造和析构函数,malloc/free无法胜任. 2.使用new遵循原则: a.用new申请的内存,必须用de

  • C++中的new/delete、构造/析构函数、dynamic_cast分析

    1,new 关键字和 malloc 函数区别(自己.功能.应用): 1,new 关键字是 C++ 的一部分: 1,如果是 C++ 编译器,则肯定可以用 new 申请堆空间内存: 2,malloc 是由 C 库提供的函数: 1,如果没有相应的库,malloc 将不能使用: 2,有些特殊的嵌入式开发中,少了 C 库,则就不能动态内存分配: 3,new 以具体类型为单位进行内存分配: 1,面向对象中一般用 new,不用 malloc: 4,malloc 以字节为单位进行内存分配: 5,new 在申请内

  • C++深入讲解new与deleted关键字的使用

    目录 可执行程序的虚拟地址空间 1.new的运算符用法(关键字) 2.new的函数用法 3.定位new 4.new创建对象 5.delete 6.对于内置类型new/delete/malloc/free可以混用 7.使用注意事项 可执行程序的虚拟地址空间 内核:存放操作系统 栈区:函数的形参,非静态的局部变量,函数现场 保护数据等等,栈是向下增长的. 共享库的内存映射区域:用于装载一个共享的动态内存库.用户可使用系统接口创建共享内存,做进程间通信. 堆区:用于程序运行时动态内存分配,堆是可以上增

  • C++ 使用new与delete需注意的原则

    C++的动态内存管理是通过new和delete两个操作来完成的,即用new来申请空间,用delete来释放空间.在使用new和delete时,注意以下原则. 1.new与delete需一一对应 用new操作申请空间,如果申请成功,必须在以后的某个时刻用delete释放该空间,既不能忘记释放,也不能多次释放.前者会引起内存泄露,后者会引起运行时错误.如下面的程序. #include <iostream> using namespace std; int main() { int *p; p=ne

  • 浅谈C++中各种不同意义的new和delete的使用

    目录 前言 new 到底做了什么 通过VS2022查看汇编代码进行验证 operator new 重载类内operator new 重载全局 ::operator new 直接调用operator new Placement new 删除与内存释放 使用operator new创建对象该如何释放 使用placement new创建对象时该如何释放 针对数组的创建和释放 系统维护开销 总结 前言 有时候我们觉得,C++术语仿佛是要故意让人难以理解似的.这里就有一个例子:请说明new operato

  • 浅谈图像处理中掩膜(mask)的意义

    刚开始涉及到图像处理的时候,在opencv等库中总会看到mask这么一个参数,非常的不理解,在查询一系列资料之后,写下它们,以供翻阅. 什么是掩膜(mask) 数字图像处理中的掩膜的概念是借鉴于PCB制版的过程,在半导体制造中,许多芯片工艺步骤采用光刻技术,用于这些步骤的图形"底片"称为掩膜(也称作"掩模"),其作用是:在硅片上选定的区域中对一个不透明的图形模板遮盖,继而下面的腐蚀或扩散将只影响选定的区域以外的区域. 图像掩膜与其类似,用选定的图像.图形或物体,对处

  • 浅谈Java中Collection和Collections的区别

    1.java.util.Collection 是一个集合接口.它提供了对集合对象进行基本操作的通用接口方法.Collection接口在Java 类库中有很多具体的实现.Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set 2.java.util.Collections 是一个包装类.它包含有各种有关集合操作的静态多态方法.此类不能实例化,就像一

  • 浅谈javascript中的Function和Arguments

    javascript的Function 属性: 1.Arguments对象 2.caller 对调用单前函数的Function的引用,如果是顶层代码调用,  则返回null(firefox返回undefined).  注:只有在代码执行时才有意义 3.length 声明函数是指定的命名参数的个数(函数定义是,定义参数的个数) 4.prototype 一个对象,用于构造函数,这个对象定义的属性和方法  由构造函数创建的所有对象共享. 方法: applay() --> applay(this,[])

  • 浅谈Javascript中的函数、this以及原型

    关于函数 在Javascript中函数实际上就是一个对象,具有引用类型的特征,所以你可以将函数直接传递给变量,这个变量将表示指向函数"对象"的指针,例如: function test(message){ alert(message); } var f = test; f('hello world'); 你也可以直接将函数申明赋值给变量: var f = function(message){ alert(message); }; f('hello world'); 在这种情况下,函数申明

  • 浅谈js中的引用和复制(传值和传址)

    好像一般很少人讲到js中的引用和复制,不过弄清楚这个概念可以帮助理解很多东西 先讲一下很基础的东西,看看js中几种数据类型分别传的什么 引用:对象.数组.函数 复制:数字.布尔 字符串单独说明,因为它的特殊性,无法确定是传递引用还是复制数值(因为字符串的值是没法改变的,所以纠结这个问题也是没意义的)但是用于比较的时候显然是属于传值比较(稍后具体说比较的事) 下面讲一下在使用中的具体体现 最普通的使用就是赋值了 var a = 1; var b = a; //赋的是a的复制值 b ++; aler

  • 浅谈c++中的stl中的map用法详解

    Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,它完成有可能在我们处理一对一数据的时候,在编程上提供快速通道.这里说下map内部数据的组织,map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的,后边我们会见识到有序的好处. 下面举例说明什么是一对一的数据映射.比如一个班级中,每个学生的学号跟他的姓名就存在着一一

  • 浅谈C++中的mutable和volatile关键字

    1.mutable 在C++中,mutable是为了突破const的限制而设置的.被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改.mutable在类中只能够修饰非静态数据成员. #include <iostream> using namespace std; class test { mutable int a; int b; public: test(int _a,int _b) :a(_a

  • 浅谈Java中的this作为返回值时返回的是什么

    有时会遇到this作为返回值的情况,那么此时返回的到底是什么呢? 返回的是调用this所处方法的那个对象的引用,读起来有点绕口哈,有没有想起小学语文分析句子成份的试题,哈哈. 一点点分析的话,主干是"返回的是引用": 什么引用呢?"那个对象的引用": 哪个对象呢?"调用方法的那个对象": 调用的哪个方法呢?"调用的是this所位于的方法":这样就清楚了. 再总结一下就是,this作为返回值时,返回的是调用某方法的对象的引用,这

  • 浅谈Python中带_的变量或函数命名

    Python 的代码风格由 PEP 8 描述.这个文档描述了 Python 编程风格的方方面面.在遵守这个文档的条件下,不同程序员编写的 Python 代码可以保持最大程度的相似风格.这样就易于阅读,易于在程序员之间交流. python中的标识符可以包含数字.字母和_,但必须以字母或者_开头,其中以_开头的命名一般具有特殊的意义. 前后均带有双下划线__的命名 一般用于特殊方法的命名,用来实现对象的一些行为或者功能,比如__new__()方法用来创建实例,__init__()方法用来初始化对象,

随机推荐