“...并没有遇到用堆比用栈更有优势的情况,所以就很疑惑。”
栈让你分配和释放简单化,但是他有一个重大缺点:释放的次序是固定的,必须是分配次序的反序。
假设栈上对象分配次序是ABCDEF。 可能在某些情况时,我释放F的时候,A可以释放了但其他对象都不能释放;某些情况时我释放F的时候,B可以释放了但其他对象都不能释放.... 对不起 这些栈上对象都不能提前释放:ABCD必须等待F和E释放了之后才能释放。。
换而言之,栈的分配和释放非常迅速(一个函数内栈的分配和回收 各自只需要一条指令),但是释放非常不灵活,极易造成浪费。越是复杂的程序,函数调用栈越深的情况,你将越会频繁的遇到这种函数A call 函数B,B知道A的某个局部变量可以提前回收内存了但是却无能为力除非B返回的场景。
所以,操作系统api都会限制栈的最大大小来让你浪费有个上限度,并且提供了“堆”。事实上,在栈出来之前,所有的内存都是堆,所有的内存都是供程序员自由的分配和释放。然而堆的管理显然复杂的多,因为栈的已分配和未分配内存各自是连续的一块,堆却是很多块已分配和未分配的内存混杂在一起,需要程序员去在这些内存块里标记他们的长度,分配时选择或者分割一个合适的块,回收时合理的进行合并。所以高级语言出来把栈和堆分开之后,第一件事就是得封装堆,glibc就是干这事。
浪费内存或者花点cpu时间来做分配&回收,享受编程书写的简单或者管理内存的高效,这也是笔者在有栈协程(可以突破单个线程栈大小的限制)和无栈协程之间抉择时的一点感受吧。
事实上,即使在堆上面,现代化的高级语言也会有一些桎梏让你并不能随意的,随时随地的释放你认为可以释放的内存,很多语法概念中都蕴含了“栈”的概念。
例如,一个C++的对象X,他有数据成员a和b, 现在我知道我不再使用X.a了,但是我还会使用X.b,有没有办法去“浅释放”X.a所占的sizeof(X.a)的内存呢?抱歉不行,你只能先释放X,在释放X的过程中去释放X.a和X.b(不能只释放一个)。而且这和X在堆还是在栈上分配没有任何关系。
换而言之,面向对象语言中的“对象”,从他的成员的构建过程中蕴含了“栈的概念”,所以他的释放依然受制于栈的先入后出的规则影响。类似于默认生成的析构函数总是先调用对象的析构函数再去遍历调用成员的析构函数一样,只是可以把这个栈看成分叉栈,或者树的先序遍历。
无垃圾回收的函数语言也不例外:函数表达式更是一个栈展开&回卷的过程,函数返回值这块内存(那些喜欢掉书袋的“纯函数语言”只有函数返回值这唯一一个显式内存分配机会)的回收机理依然受制于栈的先入后出的规则影响。而且这和返回值是存储在堆还是在栈上没有太多的关系,在堆上只可能让内存回收更加延迟的(如果编译器实现的不好)。