【C++】动态内存(一)堆内存中的new与delete的使用

一、内存概述

内存分为 栈内存堆内存

栈内存的特点:更好的局部性,对象自动销毁
堆内存的特点:运行期动态扩展,需要显式释放

所谓的动态内存,指的是堆内存,因为堆内存上的空间可以在运行期间进行改动。

在这里插入图片描述
系统在分配内存时,如上图,栈内存通常是从下往上生长(分配)的;而堆内存通常是从上往下(也就是地址由小到大)分配的。

栈内存通常是自动分配的,如:定义一个函数,以及定义一个数据。
而堆内存的使用需要用到特殊的指令:newdelete。这样的对象操作其实可以看做与类中的对象操作类似,都需要定义之后手动销毁。(类中有构造函数以及析构函数,也是构造之后进行销毁)
对象的构造分成两步:分配内存在所分配的内存上构造对象;对象的销毁与之类似。

二、new的几种常见形式:

new的几种常见形式:

  1. 构建单一对象/对象数组
  2. nothrownew
  3. placement new
  4. new auto

1.构建单一对象/对象数组

如:

int *y=new int(2)delete y;

该代码语句做的事情包括:用new指令在堆内存中制定了一串空间作为int类型,而制定多大的空间取决于int类型的大小,在c++中,int类型的大小为4个字节,所以此处在堆内存中找到了连续的4个字节的空间,并将该4个字节解释为int类型,之后将2这个值赋予在这个空间,并将4个字节的首地址返回给int类型的指针y(该语句中小括号与大括号是等价的,都是赋值,而中括号是数组的意思,在下面的例子中有体现)。

如:

int *y=new int[5]{}delete[] y;

该代码语句做的事情包括:用new指令在堆内存中找到一处连续20个字节的空间,并把每4个字节解释为int类型的数据,并且赋值一个缺省值(因为该语句没有给出赋什么值)。并且把这段连续空间的第一个字节的地址返回给int类型的指针y。可以通过y[0]访问第一个数据,y[2]访问第三个数据,诸如此类。

2.nothrow new

当堆内存中仅剩一些小块的内存,然而我们想要一块大的整块内存的时候,程序就没有办法为我们分配这样的内存了。
这时候程序就会分配错误,然后抛出异常。
如:我们仍然执行以下这个程序,希望程序为我们分配整块的20个字节的内存,但是编译器发现已经没有整块的20个字节的内存可以利用,可以给我们分配了,这时候程序会直接抛出异常,然后进入到异常处理环节,也就是执行完“int *y=new int[5]{}”一句之后就直接进入到异常处理环节,之后的任意行都不会被执行。

int *y=new int[5]{}delete[] y;

但是如果我们这样写:

#include <new>   //之所以要包含这个头文件,是因为std::nothrownew这个对象是在这个文件中被定义的,想要用这个对象必须有这个文件
int *y=new {std::nothrow} int[5]{}delete[] y;

当分配不成功的时候,y就会被赋予为空指针nullptr,而程序不会进入异常。所以我们通常会有一个y如果等于空指针的判断,告诉程序如果分配不成功,应该如何做。
也就是用了nothrow之后,即使内存分配失败,程序也仍然可以在我们的掌控之中,而不会直接强制进入到异常处理中。
如:

#include <new>   //之所以要包含这个头文件,是因为std::nothrownew这个对象是在这个文件中被定义的,想要用这个对象必须有这个文件
int *y=new (std::nothrow) int[5]{}if(y==nullptr)
{
	//告知程序如果分配失败该如何做
}
delete[] y;

3.placement new

前文强调过,new delete指令有两个步骤,第一步是分配内存,第二步是给内存赋值。
这两部一定是分开进行的,
比如vector类,是动态数组,一开始我们可能只有一个数,那么他只会分配1个数据的空间,而在程序运行过程中如果需要添加第二个数,那么程序就需要在内存中找到两个数据类型的空间,然后把第一份数据拷贝到第二个空间的第一个数据位,然后在把第二个空间的第二个数据位进行赋值。理论上来说是要这么做的,
实际运行时会有优化,即:vector每次回开辟两倍的空间,比如我需要一个数据,那么他会开辟两个数据的空间,那么第二个数据来的时候就不需要重新找内存然后拷贝,再赋值了。
但是第三个数据来的时候,仍需要重新找找到6个数据位的空间,然后把前两个数据拷贝到新内存的第一二个数据位,然后再对第三个数据位进行赋值。
这时候就会出现:后面三个数据为虽然已经开辟,但是并没有被赋值的情况。那么下次就需要找到这块内存的第四个数据位,对其进行赋值。所以需要有一种语法来显示地支持这样的操作,即:对已经分配好的内存去赋值对象地操作。
如:

	char ch[sizeof(int)];
	int* y = new (ch)int(4);

该代码首先在栈上分配了一块内存用于保存一个大小为sizeof(int)的char型的数组ch。
第二句中(ch)表示的是ch的地址,或者说指针。在new后面添加一个地址,意思是告诉new指令,不用重新分配内存,你就用我指定的内存地址,在这个地址后面分配一个存放一个int的空间,并且赋值为4。然后把该地址返回给int型的指针y。

4.auto new

如:

	int* y = new auto(4);

使用auto时会自动根据后面的赋值数据推导出数据类型。

三、new与对象对齐

如:

	struct alignas(256) Str{};
	int main()
	{
		Str* ptr =new Str();
}
struct alignas(256) Str{}; 这一句的意思是规定了str的内存地址开头一定要是256的整数倍
而用new开辟他的内存空间是可以做到这一点的。
运行多次打印出str的地址,可以发现可能会变可能不会变,但是一定是256的整数倍。
这就是new与对象的地址对齐的用法。

四、delete常见用法:placement delete

常见的场景还是vector中
如果已经分配了100个数据大小的内存,并且100个上面都已经赋值。这时希望删掉最后一个数据。
这时不可取(太慢)的方法是:把最后这块数据销毁并且把该内存归还给程序,然后另外开辟99个数据大小的内存,把这些数据拷贝过去。
可取的办法是:把数据销毁之后不把内存归还给程序,这样不用重新拷贝,并且下次需要增添数据的时候仍然可以使用placement new来增添数据。
实现方法:
一般不需要手动显示实现。

五、使用new与delete的注意事项

  1. 根据分配的是单一对象还是数组,采用相应的方式销毁。
  2. 如果x赋值为nullptr,那么delete x是合法的,这一步骤什么都不会做。
  3. 不能delete一个非new返回的内存
  4. 同一块内存不能delete多次
  5. 只能delete new直接返回的内存,其他赋值过去的地址也是不可以的
int *ptr=new  int(5)delete ptr;

ptr是什么?ptr是存放在栈中的一个数据,他存放的数据是一个地址,这个地址是一个堆内存中存放int数据类型的地址。delete ptr指的是delete会读取ptr的内容,也就是那个地址,通过这个地址找到该内存并将其中的数据清理掉,然后将该内存返还给系统。delete并不改变ptr的值。
所以执行完delete ptr之后ptr仍然存在在栈内存中,并且他的值仍然为那个内存块的地址。发生改变的仅仅是那个地址已经没有数据,并且不归程序所有,而是归系统所有了。

但是如果程序出现同一个delete语句会调用好多次的情况,而同一块内存又不可能被多次释放,此时就需要如下操作:即,再delete之后就把ptr赋值为空指针。那么即使再次会有地方调用delete ptr,也没有任何问题了,因为第二点中说了,delete一个nullptr是合法的,是没有任何效果的。

int *ptr=new  int(5)delete ptr;
ptr=nullptr;
  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

兜兜里有好多糖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值