CPU眼里的:堆和栈

“Heap和Stack是程序运行的幕后英雄,但如果它一直是“无名英雄”的话,也会成为程序员的“隐形杀手”,让我们用CPU的眼睛,把它们看个清清楚楚”
 
01
提出问题
本系列,前面的文章中,我们提及了很多次:函数“堆栈”(stack),可以说没有“堆栈”这种特殊的数据结构,就没有函数调用 。
阿布也特别喜欢“堆栈”这个翻译,因为它形象的描述了“堆栈”的堆叠结构,很好的展示了该数据结构的特点 。
但为了避免跟本章节讨论的另一个数据结构“堆”(heap)混淆,在本章节,我们一律将“堆栈”(stack)简称为“栈” 。
其实,“堆”(heap)和“栈”(stack)并不是一个陌生的话题,相反,它们在编程实践中,经常被程序员提及 。因为,不管你是否意识到它们的真实存在,你都在使用它们 。因为它们如此重要,所以为了正确的区分、使用它们,很多同学都对它们的规则、特性,倒背如流 。
这里我们将尝试用CPU的视角,重新认识一下“堆”和“栈” 。希望能减轻一点记忆的痛苦,并给你带来一些不同的启发 。
注意:我们这里说的堆和“栈”,是指程序运行所必备的“堆”和“栈” 。不是由程序员自己编写的“堆”或“栈”的数据结构,虽然二者的原理相同,但使用场景并不一致 。
 
 
02
“栈”的分析
好了,一切从程序的运行开始,我们编写一个世界上最简单的代码:
int mAIn(){//thread A}经过编译器编译后,生成的可执行程序是a.out,随后我们双击运行 。
在操作系统将a.out中唯一的函数main,加载到内存后;尽管我们没有定义任何变量、也没有进行任何函数调用,无论我们的程序需要与否,操作系统都会附送一段内存块给我们:

CPU眼里的:堆和栈

文章插图
 
这就是“栈” 。当然这个内存块不大,有几十KB的,也有几MB、几十MB的 。具体大小,一般由操作系统决定 。
视频“CPU眼里的:函数调用”告诉我们:这个“栈”,未来将承载着记录:函数返回地址、提供临时变量的内存空间等职责 。所以,只要我们使用函数,这个“栈”就必须存在,它是函数运行的前提 。
如果此时,我们又创建了一个线程B,会发生什么事情呢?
CPU眼里的:堆和栈

文章插图
 
如你所见,我们也需要为线程B提供一个类似main函数的起始运行函数thread_main 。
所以,为了保证线程B可以顺利的使用函数,操作系统也会为线程B准备一个同等大小的内存块,用来作为线程B的“栈” 。从此之后,主线程A和线程B就可以独立运行了 。
如果再增加一下难度,让线程A和线程B,同时调用函数func:
CPU眼里的:堆和栈

文章插图
 
那函数func在执行完后,它怎么知道要返回到函数main,还是返回到函数thread_main呢?
其实它们的返回地址,分别存储在线程A和线程B的“栈”里面 。所以,它们不会相互干扰,这样,线程A调用完函数func后,会根据自己“栈”中的信息,返回到函数main;线程B调用完函数func后,则会返回到函数thread_main 。
那会不会因为线程A、线程B,对函数func的调用顺序不同,而导致函数func的返回值不同呢?
CPU眼里的:堆和栈

文章插图
 
如你所见,尽管线程A和线程B,运行的都是同一个函数func的代码 。但函数func里面的变量a,却分别保存在线程A和线程B的“栈”里面,它们是完全独立的,不会相互干扰,所以,函数func返回时,变量a的值一定是:1 。当然,如果变量a是static的,那就另当别论了 。
总的来说,“栈”在使用起来,往往是自动、无感、高效的 。每次的函数调用、分配临时变量,都是在申请“栈”内存;每次函数返回,则是在释放“栈”内存 。


推荐阅读