本节书摘来自异步社区《嵌入式Linux与物联网软件开发——C语言内核深度解析》一书中的第1章,第1.7节,作者朱有鹏 , 张先凤,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.7 内存管理之栈(stack)
1.7.1 什么是栈
我们常常听人说堆栈,但大家一定要明确区分:堆就是堆,栈就是栈。我们平常说的堆栈一般是指栈。那栈的本质是什么?栈是一种数据结构,C语言中使用栈来保存局部变量(注意,下文中不强调的情况下,局部变量均指非静态局部变量)。栈是被发明出来管理内存的,是一种维护内存的机制,这就是栈的本质。
1.7.2 栈管理内存的特点(小内存、自动化)
栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的后出来,也就是先进后出,而与栈特点相对的就是先进先出,也就是队列。队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。
先进后出FILO first in last out 栈
先进先出FIFO first in first out 队列
为了大家更形象地理解,我们给出两张图。栈就好比往一个容器里面放东西,只有一个口,先放进去的只能后来拿出,后来放进去的,可以最先拿出。而队列就好比一列火车或者排着队列的军队。走在最前面的也是最早通过隧道的,先进先出。
栈
队列
1.7.3 栈的应用举例:局部变量和函数调用
C语言中的局部变量是用栈来实现的。
我们在C语言中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用。分配时栈顶指针会移动出4个字节空间给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a关联起来,对应的栈操作为入栈,就是将数据存入变量a中。需要注意的是,这里栈指针的移动和内存分配是自动完成的,不需要程序参与。
然后等我们函数退出的时候,局部变量就会被释放。对应的操作是弹栈(出栈)。出栈时也是栈顶指针移动,将栈空间中与a关联的那4个字节空间释放,这个动作也是自动的,不需要人为干预。所以我们在写代码的时候,一定不要从被调函数返回一个局部变量的地址给主调函数,因为在函数执行完后局部变量就释放了,这个地址里面的内容有可能被新的内容填充。这时如果你在主调函数里面使用它,就很有可能造成数据错误。
除了保存局部变量,栈对于函数调用来说也是至关重要的,栈保存着函数调用所需的所有维护信息。我们都知道,函数调用时,程序跳到被调函数内部,执行完后返回当前位置接着执行。这就好比你去一个陌生的地方探险,我们的目的是去寻求刺激,但是我们最后还是希望可以平安回家。在旅行前就必须做一些准备(如多带水、带指南针,等等)。我们的函数在调用时,跳到被调函数之前也是要做诸多准备的。对于函数来说,最起码它要找到回家的路,也就是被调函数的下一行代码的地址(为返回做准备),以及当前相关的局部变量值、寄存器的值等重要信息。之所以保存这些信息是怕在被调函数运行时,由于子函数会用到与主调函数一样的寄存器,而造成主调函数正在使用的寄存器数据破坏,在函数返回时,栈里面的数据弹出,即使寄存器被用过也没关系,弹出数据会使寄存器里面的值覆盖为调用以前,从而复原调用以前的现场。这些值保存在哪里呢?就保存在栈里。在函数调用时,将这些东西压入栈,在被调函数执行完,栈再弹出这些值。
以栈方式管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言(背后的运行时系统)会自动完成。
分析一个细节:在C语言中,定义局部变量时如果未初始化,则值是随机的。为什么?
定义局部变量,其实就是在栈中通过移动栈指针,来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不初始化,里面的值就是一个垃圾值。由此我们扩展一下,其实不仅仅是局部变量,所有的变量在定义时只是在内存中分配一块空间,并没有对这块空间进行任何的初始化。如果这块内存以前被用过,里面的数据还在,那它对于我们来说是没有任何意义的垃圾值。而且有时候这些数据会对我们的编程造成错误。所以我们一定要初始化变量,也就是用新的、有用的数据覆盖掉以前的数据。可能你会有个问题,那些以前用过的内存经过操作系统回收后,为什么里面还有数据。其实操作系统仅仅是回收这些内存,告诉其他程序可以用了,但并不删除这些内存里面的数据。
C语言通过一个小手段来实现局部变量的初始化。
int a = 15; // 局部变量定义时初始化
C语言编译器会自动把这行转成:
int a; // 局部变量定义
a = 15; // 普通的赋值语句
1.7.4 栈的约束(预定栈大小不灵活,怕溢出)
首先,栈是有大小的。而且栈的大小是可以设置的,只是栈内存大小不好选择。如果太小怕溢出,太大怕浪费内存(这个缺点有点像数组)。
其次,栈的溢出危害很大,一定要避免。所以,在C语言中定义局部变量时不能定义太多或者太大(如不能定义局部变量时int a[10000];使用递归来解决问题时一定要注意递归收敛)。