内存篇之堆与栈的绕口令

    堆(heap)/(stackcall stack)是两块功能完全不同的系统内存区,堆内存是由malloc/free函数动态申请和回收,而栈则是编译器与启动代码或线程创建代码配合,约定用CPU某寄存器标识最新使用位置的一段内存(所以分系统栈与任务栈,见后文)。但不知是谁添乱,用堆栈这个词代表的含义,导致中文里堆栈混在一起,三者的关系就象绕口令,堆为堆,栈是栈,堆栈是栈不是堆。为避免混淆,后文一律不用堆栈,而用/表示。

    注:栈有时也指一种数据结构,特点是只从一端(栈顶stack pointer)进行数据推入(push)和弹出(pop)操作。由于只允许单端操作,因而遵循后进先出(LIFO, Last In First Out)原则,(想想为什么),好比弹匣里压子弹,后压进的子弹先发射。而形成对比的队列结构则是从两端分别读写数据,因而先进先出(FIFO),好比火车过隧道,车头先进,也是车头先出。

    本文所说的栈更确切的说法是栈段(stack segment),指采用这种结构工作的一段物理内存,用于存放局部变量函数返回地址等。栈段只需通过栈顶指针即可访问,一些CPU有专用寄存器用于存放栈顶地址,另一些则定义某种规范,让所有此CPU的编译器都默认使用某通用寄存器(如ARMR13)存放sp指针。

    堆/栈的区分在相关bbs上似乎是一个永恒的话题,可见大量初学者对此混淆不清。这是因为一来虽然堆/栈是软件最基础的概念之一,却很少有书详细论述,多数是只言片语;再者很多人觉得堆/栈与具体编程无关,不需要深入研究。然而编程时即使写出完全一样的代码,不同人的底气也不一样:有人有理有据所以胸有成竹,有人纯粹瞎蒙写完听天由命。而深刻理解堆/栈,是跳出代码搬运工层次的必需一步。看下面例子:

void alg_proc(char *infile, char *outfile)

{  

  char tmp_inbuf[3000000],tmp_outbuf[3000000];

  FILE *input=fopen(infile,”rb”);

  FILE *output=fopen(outfile,”wb”);

  fread(tmp_inbuf, 1, 3000000, input);

  algfun(tmp_inbuf, tmp_outbuf, BUF_LEN);

  fwrite(tmp_outbuf,1,3000000, output);

  ……

}

    乍一看没什么问题,初学者习惯用数组,不愿碰指针,反正功能实现了,有容易的何必用难的J。但理解堆/栈后就会知道,这段程序在某些环境下可能无法运行。为什么?暂时卖个关子,先从几方面对比理解堆/栈。

/栈初始化

    堆/栈都是在系统的启动代码里预留并初始化,初始化阶段结束后,系统就能为用户提供函数调用背后的进出栈支持以及动态堆内存管理支持。堆的初始化与后续管理方式相关,设定初始默认堆大小后,根据不同管理方式初始化链表/位图/内存池甚至OS内核里的相关结构和参数。栈是单端存取,初始化时只需设定栈顶地址(stack pointer,简称SP)和栈最大容量,具体说是由启动代码和内存布局脚本文件配合,从系统整体内存中划定一块区域,并用一个CPU寄存器保存SP,可以是专门的硬件SP寄存器(如x86)或者标准规范指定某通用寄存器专门存储SPARM),具体栈初始化过程后面专文论述。注意这里指的是系统栈,任务栈是在线程创建时由用户任意分配,不需要初始化。

分配与回收

    堆内存借助malloc/free函数分配和回收,这里的分配和回收,不象小孩子分糖果,分完就没有了。堆内存只是被暂时使用,是对内存使用权的虚拟分配/回收,就象宾馆把代表房间使用权的钥匙分给客人,客人结帐时还要交还钥匙。(站在管理者视角是分配/回收,用户视角为申请/释放,实际一回事)。用户通过malloc从堆内存区拿到某块内存的钥匙,这块内存此后就被用户独占,可在上面存储数据,使用完则调用free,系统收回内存钥匙。malloc/free在程序运行中动态调用,所以堆都是动态分配,没有静态堆。

    栈内存是编译器自动分配和回收的内存区:栈区所保存的元素大小可统计,如局部变量/数组/函数参数等,所以编译器就能提前把所有元素在栈中的位置安排好。相对堆在运行时才申请空间,栈是一种事先的离线的静态分配(或者说安排更恰当)。做到这点还依赖于栈的单端存储结构,不需要专门提供地址,按LIFO原则挪动相应SP指针就能实现内存的安排:压栈是往SP指针所指内存写数据,并递增或递减SP,出栈是把数据从SP所指内存读到寄存器,并递减或递增SP(具体过程见本章节5)。

    堆的分配和使用都在运行阶段发生,即先通过malloc返回一块buffer的指针,然后用这块buffer承载数据。栈在运行时直接使用,即通过sp指针读写栈内存。打个比方,堆分配是临时买了一堆东西,然后在家里到处找地方放;而栈是事先计划好买多少东西,根据每件物品大小划定好对应的空间,买好后不需要找,直接一件件放到事先规划好的位置。事先规划可以一个萝卜一个坑,但计划又往往赶不上变化,所以堆/栈两种方式都不可缺少。

空间大小

    为防浪费,系统栈空间一般较小,因为大容量栈固然能避免栈溢出,但程序触及的最大栈深如果远达不到系统栈底,剩下的部分就成了乌鸦喝不到的水。看开头例子,函数中的局部数组char tmp_inbuf[3000000],意味着要在栈中存储3000000bytes数据,这已超出多数系统栈的最大容量,因此是不实用的代码。更进一步,涉及多线程编程时,线程栈的设置直接考验程序员对栈的理解,要根据线程里函数调用深度及局部变量使用情况估计栈的极限占用,若设置过小,函数执行到一定深度会发生栈溢出。

     堆大小只受限于计算机物理或虚拟内存,容量一般很大。只不过注意malloc失败会返回空指针,最好做针对性处理,主动给出提示并避免内存不足时程序crash

    软件,尤其是嵌入式软件中,必须清楚自己的代码占用了多少堆/栈资源,否则,程序可能中看不中用。

分配效率

    堆管理是C函数库实现的功能,属于在线分配,尽管不同实现机制间效率有高有低,但总归要占用运行时资源,如位图查找/链表查找等。而且频繁malloc/free后往往会会造成内存碎片,需要不定时整理。如果要进行系统调用,更会引发用户态和核心态的切换,使整体分配效率更低。

    而栈是在编译时划定空间,属于离线分配,不占用运行时资源,没有运行时代价。

    所以单就分配效率来说,栈优于堆。

访问效率

    有人认为栈的访问效率一定高于堆,这是不对的。首先要把分配与使用区分开,不能混为一谈。如上文,栈的分配效率高于堆,因为其根本不在运行时分配。但内存的真实读写效率取决于其物理属性,堆和栈具体谁的访问效率高取决于初始化阶段或线程创建阶段(即系统栈/任务栈),系统把堆/栈各自定位到哪段物理内存上,包括要考虑cache的影响。

    所以如果所处物理内存相同,堆/栈的访问效率相当。笼统说法中栈效率高于堆是综合分配与访问,毕竟栈没有分配的代价。

总结

    栈由系统(编译器)管理,不需要程序员运行时显式分配/释放,使用方便且整体效率高,缺点是容量一般较小,不合适大数据缓存,且栈是编译器静态预先使用,不如动态堆使用灵活;堆是运行时函数库和OS提供的功能,灵活方便,适合灵活存储动态大数据,但需要一定管理代价,且易发生堆泄漏。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值