C / C++系列 (3):heap vs. stack & new vs. malloc

文章参考:细说new和malloc区别
Linux虚拟地址空间布局以及进程栈和线程栈总结

在内存分配中会涉及到栈和堆(和数据结构中的堆栈不同),所以先介绍以下在内存中栈和堆的区别

heap vs. stack

key points:

  • 堆(heap):持久化、顺序随意、全局
  • 栈(stack):临时、 后进先出、局部

1、申请方式

  • 堆:由程序员自己申请并指明大小的,当进程调用malloc / new等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free / delete等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
  • 栈:系统自动分配的,如声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间,有以下三个方面的用途:
  1. 为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
  2. 记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame)或过程活动记录(Procedure Activation Record)。它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段。
  3. 临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存。

2、分配方式

  • 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内 存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大 小,系统会自动的将多余的那部分重新放入空闲链表中。所以堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历
  • 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出(大错误不好处理)。所以栈向低地址扩展(即”向下生长”),是连续的内存区域。在linux中使用ulimit -s 命令可以查看和设置堆栈的最大值。

3、申请大小限制

  • 堆:由低地址到高地址扩展的数据结构,是不连续的内存区域,这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
  • 栈:在windows下,栈是由高向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在windows下,栈的大小是2M(也有的说是10M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

4、申请效率

  • 堆:由类似new机制来分配内存,一般速度比较慢,而且容易产生内存碎片,这一点从分配机制上能解释,不过用起来比较方便。
  • 栈:由系统自动分配,速度比较快,而且程序员是无法控制的。

5、碎片问题

  • 堆:容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。
  • 栈:不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。

总结:

  • 堆和栈都是内存,堆需要手动分配,比如使用外部内存芯片,使用malloc函数分配,自由度高,比较方便,但是里面像是一个散沙有碎片。
  • 栈则是编译器自动分配的,比较方便,但是是一个黑盒。这两种各有好处,如果实在想要全是栈,那其实本质上还是要先认为的使用一个封装好的堆。

new vs. malloc

1、使用范围

  • new / delete是C++中独有的操作符允许重载,返回值为指定类型的指针
  • malloc / free是C/C++中的标准库函数不允许重载,返回值只是单纯的void地址

2、参数

  • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。
  • 使用malloc则需要显式地指出所需内存的尺寸。

3、分配内存空间

  • new操作符从自由存储区(free store)上为对象动态分配内存空间
  • malloc函数从堆上动态分配内存。

自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

4、分配内存方法

  • 使用new创建对象在分配内存的时候会自动调用构造函数,同时也可以完成对对象的初始化,同理要记得delete也能自动调用析构函数。
  • 因为malloc和 free是库函数而不是运算符,不在编译器控制范围之内,所以不能够自动调用构造函数和析构函数。也就是mallloc只是单纯地为变量分配内存,free也只是释放变量的内存。

5、分配失败的返回值

  • new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;
  • malloc分配内存失败时返回NULL。

所以malloc分配内存后判断分配是否成功可以通过返回值是否为NULL,但是new根本不会返回NULL,而且程序能够执行到if语句已经说明内存分配成功了,如果失败早就抛异常了。正确的做法应该是使用异常机制(挖坑)

6、对数组的处理

C++提供了new[]与delete[]来专门处理数组类型:

A * ptr = new A[10];//分配10个A对象

使用new[]分配的内存必须使用delete[]进行释放:

delete [] ptr;

new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用 ,不然会找出数组对象部分释放的现象,造成内存泄漏。
至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

int * ptr = (int *) malloc( sizeof(int) );//分配一个10个int元素的数组
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值