栈的定义
栈(stack)又名堆栈,堆栈是一个特定的存储区或寄存器,它的一端是固定的,另一端是浮动的 。对这个存储区存入的数据,是一种特殊的数据结构。所有的数据存入或取出,只能在浮动的一端(称栈顶)进行,严格按照“先进后出”的原则存取,位于其中间的元素,必须在其栈上部(后进栈者)诸元素逐个移出后才能取出。在内存储器(随机存储器)中开辟一个区域作为堆栈,叫软件堆栈;用寄存器构成的堆栈,叫硬件堆栈。
- 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收,分配方式倒是类似于链表。
本文目录:
栈的特点——先进后出
栈是一种一端受限的线性表,它只接受从同一端插入或删除,并且严格遵守先进后出的准则。什么意思呢,我们这样来理解,栈就是一个倒置的桶,这个桶有一定的空间,我们可以用这个桶来做装很多东西。在C语言中有着形如 int 类型占4个字节的空间,char 类型占1个字节空间等等的不同大小的变量类型。而我们在函数中定义一个变量 int a = 10; ,等于我们把一个名为 a 的占据4个字节空间的物体放入栈这个桶中,那么我们再来定义一个变量 char c = \'x\',同样也放如栈中。那么我们模拟出栈的布局应是如下所示
我们可以看到,在栈中变量c在变量a的上方。因此,出栈的时候只能先出 c 变量再出 a 变量,而我们知道,a 变量是先于 c 变量入栈的,所以栈的特点为 先进后出 。
好了,关于栈的数据结构部分的讨论暂且先停下,下面我们要讨论的是内存中的栈区,而不是我们所说的数据结构栈。
函数内部的变量在栈区申请
我们说函数、全局变量、静态变量的虚拟地址在编译时就可确定,而在函数中使用的变量在运行时确定,函数形参在函数调用时确定。
那么这句话是什么意思呢?函数的入口地址、全局变量在编译阶段就确定的地址,这是编译链接的知识我们今天先不讨论,今天主要讨论一下栈区以及栈区的使用。
栈区空间分布
我们所说的栈区和平时我们用的数据结构中的栈还是有一些不同的。在内存中栈又叫堆栈,它分布在虚拟地址空间中,仅占一小部分。一个程序运行时拥有一个自己的虚拟地址空间,在32位计算机上为 2^32次方大小(4G)的一块内存空间。在这块空间中所有的地址都是逻辑地址,即虚拟的地址,在程序运行到哪一部分空间时把相应的内存页映射到真实的物理地址上。整个虚拟地址空间分布大致如下图所示
在定义变量之前,我们首先要知道,函数中使用的变量在栈上申请空间,至于原因我们下次在讨论。那么对于栈这种数据结构来说,它是由高地址向低地址生长的一种结构。像我们平时在 main函数或是普通的函数中定义的变量都是由栈区来进行管理的。下面进行几个实例以便于我们更加了解栈区的使用。
字符串在栈中申请空间的方式
编写如下C程序:
int main()
{
char str[] = { "hello world" };
char str2[10];
printf("%s \n",str);
printf("%s\n",str2);
return 0;
}
在 VS 2019中运行
我们在C源码中,给 str 赋值为“Hello World”,而 str2 没有进行赋值。
这里要说明一点,在函数内部会根据函数所用到的空间大小生成函数的栈帧,而后对其内存空间进行 0xcccc cccc 的初始化赋值。而\'cc\' 在中文编码下就是“烫”字符。有时候我们会说申请的局部变量(函数的作用域下)没有进行赋值其内容会是随机值。这么说其实也没错,原因很简单,在内存中的某个内存块上,无时无刻不伴随着大量程序的使用,而在程序使用过后就会在该内存块处留下一些数据,这些数据我们无法使用在我们看来就是随机值。而在 VS 编译器中为了防止随机值对程序运行结果造成干扰,就通过用初始化为 0xcccc cccc的方式进行统一的初始化。而字符串的输出时靠字符串末尾的 \0 结束符来确定的,str2 ,中并没有该字符,因此在输出时一直顺着栈向高地址寻找,直到找到 str 中的 \0 结束符。
变量在栈中申请空间的方式
从上图我们也可以看到字符串在栈中是连续申请的。此外,变量、数组等也是在栈中连续申请的,请看下面的实例:
在Linux 上编写如下代码:
#include <stdio.h>
void main(void)
{
//整型变量
int a = 10;
int b = 10;
//字符型变量
char c = a;
char ch = b;
//数组型变量
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int d = 10;
// 输出源码
char str[] =" \
int a = 10;\n \
int b = 10;\n \
\n \
char c = a;\n \
char ch = b;\n \
\n \
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };\n \
int d = 10;\n";
printf("%s\n",str);
printf("&a = %x \t &b = %x\n",&a, &b);
printf("&c = %x \t &ch = %x\n",&c, &ch);
for(int i = 0; i < 10; ++i)
{
printf("&arr[%d] = %x \t arr[%d] = %d\n", i, &arr[i], i, arr[i]);
}
printf("---&arr[10] &arr[11] &arr[12] &arr[13] &arr[14] &arr[15] \n");
printf("---%x %x %x %x %x %x\n", &arr[10], &arr[11], &arr[12], &arr[13], &arr[14], &arr[15]);
exit(0);
}
在Linux 上编译运行 gcc -std=c99 -o test test.c ./test 其中 -std=c99 表示使用C99语法规则。
我们把输出结果复制到画图板上进行分析。另外,程序每次运行时,程序执行的起始地址不同,最终每次输出的结果也不同(如画图板上输出结果为重新运行的结果),但是这并我影响我们观察的结果。
分析:
如图所示,在标注出的绿色部分是申请的两个整型变量,根据栈的特性和申请的顺序,变量a在靠近高地址处,变量b在靠近低地址处。a、b都是整型变量,占4字节,所以在内存中因该是 e105a30---e105a33为b变量占据的内存空间,e105a34---e105a37 为a变量占据的内存地址。
注:在图中仅标注出了起始地址。
对于 char 型字符变量 c 和 ch ,他们都只有一个字节的内存空间,但是由于内存对齐机制,他们两个一共占据了 e105a2c---e105a2f 4个字节。虽然如此,在使用时他们任然只使用一个字节。c 对应内存地址为 e105a2f ,ch 对应内存地址为 e105a2e 。
通过观察我们发现,数组中的 arr[11]、arr[12]、arr[13]的地址与我们定义的 ch 、b、a的首地址相同,这是怎么回事呢?
数组在栈中的排布
数组比较特殊,按照我们的理解,栈从高地址位向低地址为生长。按理来说,arr[0] 作为数组的首元素应该在高地址为最先被申请,而 arr[9] 为数组的末位元素理应在栈的低地址位被申请。然而,根据程序打印出的结果我们画出的栈内存布局显示,数组在栈中是顺着地址增长的方向排布的。