c++编程(8)——new、delete

欢迎来到博主的专栏——c++编程
博主ID:代码小豪

我们关于类和对象的讲解终于告一段落了,这并不意味着类和对象的内容已经概述完全了,还有许多知识点等着我们在未来进行学习。再不久的将来,博主还会继续介绍类与对象的更多用法

动态内存管理

c++的变量主要储存在三个内存区域
(1)静态区,静态区的内存是在程序编译时就完成分配的,在静态区的变量在整个程序的运行期间都是存在的,如全局变量,static变量。

(2)栈区,栈区上的内存通常是具有程序块作用域的变量,在程序进入某个函数或者分支、循环语句块中生成的局部变量函数参数也是开辟在栈区间的。当程序运行到这些变量的生命周期时,会在栈区为其开辟内存空间,当程序运行超出变量的生命周期时,会自动释放这些内存空间。

(3)堆区,从堆上分配的内存空间也称为动态内存空间,这块空间需要由程序员通过malloc,new申请。空间大小,空间存在的周期都由程序员维护,优点是使用自由,但是也是最容易出现的问题的一块的空间

若是程序员对动态内存的使用不当,会导致各种各样的问题,比如指针丢失,内存泄漏,越位访问等。

如果想要深入了解动态内存管理,可以去看看博主关于C语言动态内存的的博客,c++与c关于动态内存分配之间的差异不大,主要在于新增的new、delete关键字,因此在这里重点讲述着两个关键字。

new

new的主要作用是在堆区上申请一块空间,当你需要再堆区创建一个对象时,我们可以使用new关键字,写出在这个堆区上存储的对象类型,再用一个同类型的指针进行维护和操作。

如:

int* p1 = new int;//申请4byte

看到这熟悉c语言的朋友们就有所疑惑了,这和malloc的作用一致啊,向操作系统申请一块空间,然后管理。别急,new的作用远不止如此。

对于内置类型来说,new和malloc的差距不大,对类类型的对象的创建有所区别。

我们在vs当中可以查看到new的声明。
在这里插入图片描述
原来我们常用的new是一个重载后的操作符,我们在new后面写的类型是向new传入一个参数,这个参数是在堆区申请的字节大小。比如new int,int类型的大小是4字节,于是传入的参数即为4。这个函数的返回值是void*类型。

但是我们用来维护这块空间的指针是int类型的,但是返回值却为void,这就说明new不仅仅能开辟动态内存,还能进行类型转换。

	int* p1 = new int;//申请4byte
	int* p1 = (int*)malloc(sizeof(int));//申请4byte

new的作用就到此了吗?远远不止,我们可以在c++的操作手册当中看到operator new的定义

在这里插入图片描述
咱也不管手册当中的一大堆英文是什么意思了(网页翻译有误,不推荐)。博主在这里为大家整理出来了。operator new被重载成了三个函数

throwing (1)	
void* operator new (std::size_t size);
nothrow (2)	
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
placement (3)	
void* operator new (std::size_t size, void* ptr) noexcept

分别是(1)throwing new(2)nothrow new和(3)placement new。

(1)throwing new也就是我们最常用的new调用的函数,在手册中写道:throwing new会分配size个字节的内存,然后再这块内存的起始地址作为返回值,返回一个非空的指针。

重点在于,throwing new如果出错,不会像malloc一样,返回一个错误标志NULL,而是抛出一个异常让用户接收。(接收异常的方法将在后面讲解)。

我们都知道C语言的malloc使用起来有一个麻烦的点就是如果空间开辟失败,就会返回一个NULL作为错误标志,这就意味着大部分c程序中的malloc函数在使用之后,都要对其返回值写一个判断语句。一个程序写个数十个,上百个malloc,就要多写几白行去判断合法性,那么程序不就变得臃肿了。

(2)nothrow new则是和malloc类似,申请一块size字节的内存空间,若是申请失败,就会返回一个NULL作为错误标志,这就意味着用户在使用nothrow new之后是要做出错误检查的。

	int* p = new(nothrow)int;
	if (p == nullptr)
	{
		cout << "allocate fail" << endl;
	}

(3)placement new又是另一种用法了,这里我们留个悬念,放在后面再说。

实际上operator new在底层上是调用c函数malloc的。这里放上博主找到operator实现的源代码

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}

new和malloc的差别

在前面new和malloc的分析中我们已经发现两个差别了,一个是new会自动转换返回类型,二是new是抛出异常,而不是返回错误标志NULL。那么new的作用就到此位置了吗?当然不是,前面new的对象都是内置类型(int),我们来试试new一下类类型的对象

class A
{
public:
	A(int a = 0, int b = 0)
	{
		//用来检查是否调用构造函数
		cout << "A(int a = 0, int b = 0)" << endl;

		_a = a;
		_b = b;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;

		_a = a._a;
		_b = a._b;
	}
	~A()
	{
		cout << "	~A()" << endl;
		//用来检查是否调用析构函数
	}
private:
	int _a;
	int _b;
};

先用malloc生成对象来作为一个对照组吧。

	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;

打开vs当中的的调试,可以看看这两个对象在开辟内存之后的差异。
在这里插入图片描述
p1是由malloc函数分配的对象,而p2是由new分配的对象,我们可以发现p1当中的内存空间是未初始化的,而p2当中的内存空间是初始化好的。这是由于new开辟的对象如果是类类型的对象,那么new就会调用这个对象的构造函数完成初始化。

运行这个代码,也能看到控制台将输出构造函数当中的检查语句。
在这里插入图片描述
这证明了我们在new一个对象的时候,会调用其构造函数为其完成初始化。而且new还能带参调用构造函数

	A* p1 = new A;//调用构造函数
	A* p2 = new A(1, 2);//调用带参构造函数
	A* p3 = new A(*p2);//调用拷贝构造函数

在这里插入图片描述

placement new

placement new是最与众不同的operator new了,它不会像操作系统申请任何空间,它只会对某个空间的对象调用他的构造函数。

使用方法如下

new(ptr)type(构造函数的参数)

比如我们用operator new生成一块未初始化的空间

(注意new和operator new的区别,operator new只是new底层当中调用的函数之一,operator new是不会调用对象的构造函数对空间进行初始化的)。

	A* p1 = (A*)operator new(sizeof(A));
	//p1没有初始化

	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A;
	//p2调用构造函数

	A* p3 = (A*)operator new(sizeof(A));
	new(p3)A(*p2);
	//p3调用构造函数

通过调试可以发现,placement new只会调用指针指向的空间的对象的构造函数,而不是分配一个内存空间给对象。
在这里插入图片描述

new[]

new[]会为多个对象分配一个连续的内存空间,其特性基本与new无异,主要是对象初始化上的差异。

与new类似,new[]会返回内存块中的第一个字节位作为指针返回值。也会在内存分配失败以后抛出一个异常错误。new[]的底层是operat new[](这里不讲述,感兴趣的可以翻阅c++使用手册)
cplusplus

new[]的使用方法如下:

new type[size]

先拿内置类型的初始化举例。

int* p1 = new int[10];//分配40byte

int* p2 = new int[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//为这片空间完成初始化

int* p3 = new int[10] {1, 2, 3};
//未显示初始化的部分会以0进行初始化

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

对类类型的对象则是多次调用对象的构造函数,也可以在参数列表当中指定初始化时的参数。

	A* p2 = new A(2, 1);
	cout<<endl;//让输出变得直观
	A* P1 = new A[10]{ {1,2},3,*p2 };

{1,2}相当于A[0]对象的构造函数会调用

A(1,2);
//这只是类比,实际上这个语句是错误的语法

而(*P2)则是调用拷贝构造函数。运行这个程序就能发现构造函数被调用了十次。

在这里插入图片描述

我们在调试当中可以打开反汇编。在调用operator new[]那行指令当中可以看到new[]调用operator new[]的参数size是多少
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以看到new[]传入给operator new[]的size值是88,这就意味着new[]向操作系统申请了88个字节的空间,但是我们看看10个A类型对象的数组大小是多少。

cout << sizeof(A[10]) << endl;

在这里插入图片描述

运行发现只有80,那么这就意味着,我们new10个A类型对象的空间只有80字节,但是operator new[]却申请了88个字节,那么这个消失的8字节去了哪呢?

我们先来考虑这个问题,编译器是怎么知道要对10个A类型的对象调用10次构造函数呢?难道是vs2022装入了人工智能?当然不是,我们可以在调试当中的内存监视窗口看到端倪。

在这里插入图片描述
我们先来牢牢的记住P1的地址值,然后打开内存监视窗口,找到这个P1所在的空间
在这里插入图片描述
0x0a是16进制,转换成10进制就是10.而且这个空间,到P1的起始空间,满打满算正好是8字节(每两位16位数就是1字节)。

那么我们知道这消失的8字节去哪了,对象数组的起始地址往前8个字节的空间会被用来记录元素个数,这样调用构造函数的时候就知道要调用多少次了。

delete和delete[]

delete和delete[]就没有那么多门道了,delete的底层是operator delete,operator delete又会调用一个c函数free()。大家这里前往使用手册看看,这里不过多讲述了。

要注意在c++使用动态内存管理时,new出来的空间要用delete释放,new[]出来的空间要用delete[]释放,malloc出来的空间要用free释放。不能混着用。

	int* p1 = new int;
	delete p1;
	int* p2 = new int[10];
	delete[] p2;
	int* p3 = (int*)malloc(sizeof(int));
	free(p3);

如果new的是类类型的对象,delete会在释放空间之前,调用一次该对象的析构函数,而free则不会。

如果是new[],那么delete[]则会依次调用这些对象的析构函数,具体要调用多少次呢?这些则被记录在分配空间的前8个字节当中。

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码小豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值