new/delete vs malloc/free

new|delete

new的底层:先调malloc申请堆内存空间,然后调构造函数初始化内存空间,最后返回指针
delete底层:先执行析构函数,然后调free释放堆内存空间

析构函数和构造函数和普通函数本质是一样的,你不写就是默认的啥也没干,写了就是函数里面写了啥干啥,所以堆空间里有指针指向别的内存空间,就需要在析构函数里delete这个指针。
构造函数和new不是绑定的,在栈上分配的对象也会调用构造函数来初始化
销毁对象时都会先执行析构函数然后回收内存

定位new

一种new表达式形式,接受一些额外的参数,在new关键字后面的括号中给出。

使用 new 申请内存失败会抛出bad_alloc异常(malloc申请空间失败返回NULL)

如果不想抛异常而是返回一个空指针,可以使用定位new来申请内存,语法是:new(std::nothrow)

为什么有了malloc还要有new?

针对类对象而言,对象在创建的同时需要自动执行构造函数,对象在消亡之前要自动执行析构函数

而由于malloc/free不在编译器的控制权限内,所以不能自动执行构造函数和析构函数。所以c++又设计了能够自动执行构造函数的new,和能自动执行析构函数的delete

补充:
为什么库函数不在编译器控制权限内,而运算符在?
库函数是已经编译的代码,编译器不会再编译检查,由链接器将库同用户写的代码合成exe文件。而运算符是否正确,编译器在编译扫描分析时就可以判定。

delete与指针

我们传递给delete的指针必须指向通过new在堆中动态分配的内存,或者是一个空指针。
释放一块并非new分配的内存,或者将相同的指针值释放多次,都是错误的
在这里插入图片描述
对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。

执行delete pi1和pd2所产生的错误则更具潜在危害
通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。
对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的,所以是相当危险的

使用new和delete常见的三个问题

1、忘记delete内存
忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。

2.多次delete同一内存
当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。相对于查找和修正这些错误来说,制造出这些错误要简单得多。

3、delete了之后接着使用
使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。

开发建议:坚持使用智能指针!!!

delete之后重置指针值

当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了空悬指针,指向一块曾经保存数据对象但现在已经无效的内存。未初始化指针所有缺点空悬指针也都有。

有一种方法可以避免空悬指针的问题:
在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

但是,这只是提供了有限的保护
动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。例如:
在这里插入图片描述
本例中p和q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。但是,重置p对q没有任何作用,在我们释放p所指向的内存时,q也变为无效了。但是没有置为nullptr,是很危险的。

operator new和operator delete

new在底层调用operator new全局函数来申请空间
delete在底层通过operator delete全局函数来释放空间

对delete和delete[]的理解

  • 对于没有自定义析构器的class或者struct,可以将整块数组当成一个整体来处理,可以使用delete直接释放这一整块内存空间。
  • 而对于自定义了析构器的class或者struct,在delete时需要逐个调用其析构器,所以需要有标识位记录数组大小,然后通过数组大小来逐个调用其析构器,再释放内存。也就是需要delete[]来进行内存释放。

allocator类

使用new申请堆内存时,将内存分配和对象构造组合在了一起。
当我们希望将内存分配和对象构造分离开来时,可以使用allocator类。

allocator支持的API:
在这里插入图片描述
分配内存:
类似vector,allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
在这里插入图片描述
构造对象:
allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:

在这里插入图片描述

销毁对象:
当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数
在这里插入图片描述
在循环开始处,q指向最后构造的元素之后的位置。我们在调用destroy之前对q进行了递减操作。因此,第一次调用destroy时,q指向最后一个构造的元素。最后一步循环中我们destroy了第一个构造的元素,随后q将与p相等,循环结束。
注意:我们只能对真正构造了的元素进行destroy操作。

释放内存:
一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成:

alloc.deallocate(p,n);

我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。

malloc/calloc/realloc总结

malloc申请的空间没有初始化

calloc会对申请空间执行默认初始化,而其他两个不会。

realloc是对已经存在的空间进行调整,两种调整方式:
a.直接原地调整大小
b.重新开空间,内容拷贝,释放原有空间

堆内存分配和释放原理

malloc()申请内存,free()释放内存

  • malloc函数利用空闲链表将可用的内存块连接为一个长长的列表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二,一块大小与用户请求的大小相等,另一块的大小就是剩下的字节。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。

  • 调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针

链表式内存管理的缺点:

  • 一旦链表中的 pre 或 next 指针被破坏,整个堆就无法工作
  • 小的空闲区域往往不容易再次分配,形成很多内存碎片。
  • 经常分配和释放内存会造成链表过长,增加遍历的时间。

原理详解
在程序运行过程中,堆内存从低地址向高地址连续分配,随着内存的释放,会出现不连续的空闲区域,如下图所示:
在这里插入图片描述
带阴影的方框是已被分配的内存,白色方框是空闲内存或已被释放的内存。程序需要内存时,malloc() 首先遍历空闲区域,看是否有大小合适的内存块,如果有,就分配,如果没有,就向操作系统申请(发生系统调用)。为了保证分配给程序的内存的连续性,malloc() 只会在一个空闲区域中分配,而不能将多个空闲区域联合起来。

内存块(包括已分配和空闲的)的结构类似于链表,它们之间通过指针连接在一起。在实际应用中,一个内存块的结构如下图所示:
在这里插入图片描述
next 是指针,指向下一个内存块,used 用来表示当前内存块是否已被使用。这样,整个堆区就会形成如下图所示的链表:
在这里插入图片描述
现在假设需要为程序分配100个字节的内存,当搜索到图中第一个空闲区域(大小为200个字节)时,发现满足条件,那么就在这里分配。这时候 malloc() 会把第一个空闲区域拆分成两部分,一部分交给程序使用,剩下的部分仍空闲,如下图所示:
在这里插入图片描述
仍然以图3为例,当程序释放掉第三个内存块时,就会形成新的空闲区域,free() 会将第二、三、四个连续的空闲区域合并为一个,如下图所示:
在这里插入图片描述
可以看到,malloc() 和 free() 所做的工作主要是对已有内存块的分拆和合并,并没有频繁地向操作系统申请内存,这大大提高了内存分配的效率。

另外,由于单向链表只能向一个方向搜索,在合并或拆分内存块时不方便,所以大部分 malloc() 实现都会在内存块中增加一个 pre 指针指向上一个内存块,构成双向链表。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值