0. 问题:C语言中的数据存储区有哪些?
1. 程序中的栈
栈是现代计算机程序里最为重要的概念,其作用是用于维护函数调用上下文,也就是说在我们程序运行的时候必须要使用的存储区就是栈存储区,因为C程序是一个接着一个的函数调用,并且C程序是从main函数开始,既然使用了main函数,必然就是使用栈存储区(因为栈的作用就是维护函数调用的上下文)。 栈存储区主要用来保存函数调用的时所需要的参数信息、局部变量信息、返回地址、寄存器信息等等。如果程序中没有了栈存储区,那么程序几乎无法运行。
2. 栈的概念(与数据结果中的栈类比)
栈:一种后进先出的行为。

3. 栈在函数调用时的作用
问题: 为什么说函数调用时少不了栈?
因为函数调用时,在内存中需要维护一个活动记录,活动记录中包含了函数调用的参数,函数调用的返回地址,寄存器信息,局部变量,其他数据信息(如临时变量)等等,C语言中通过栈来维护活动记录中的信息。

4. 函数调用的过程

总结:每次函数调用都会对应着在栈上建立一个新的活动记录,调用函数的活动记录位于栈的中部,被调函数的活动记录位于栈的顶部。
esp:栈顶指针
ebp:函数调用结束的返回地址
5. 函数调用的栈变化
-
从main()开始运行,在栈中就会有main()中的活动记录;
main()运行后栈中的状态 -
main()调用f(),在栈中建立f()的活动记录;
f()运行后栈中的状态 - 当从f()调用中返回到main()
当f()运行完后返回到main()后栈中的状态
栈空间的数据不会因为函数的返回而立即的改变,它只修改了esp指针和ebp指针中的地址值,并没有去改变栈空间中的数据。
问题:为什么不可以返回函数内部变量的地址(局部变量地址)和局部数组?
因为虽然栈内存中的数据不会因为函数的返回而改变,但是如果函数返回之后,又立即调用了另一个函数,原来函数的内存空间就会给另一个函数使用,这样栈内存中的原来函数的内存空间就会发生变化。如果返回局部变量地址或局部数组是没有意义的,甚至是错误的,因为我们的指针所指向的局部变量或局部数组不存在了,这样就会造成野指针的错误而使得程序运行崩溃。
6. 函数调用栈上的数据
- 函数调用时,对应的栈空间在函数返回前是专用的;
- 函数调用结束后,栈空间将被释放,数据不再有效。
程序说明:指向栈数据的指针,函数返回后,栈中的数据没有改变
#include <stdio.h>
int* g()
{
int a[10] = {0};
return a;
}
void f()
{
int i = 0;
int b[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* pointer = g();
for(i=0; i<10; i++)
{
b[i] = pointer[i];
}
for(i=0; i<10; i++)
{
printf("%d\n", b[i]);
}
}
void main()
{
f();
return 0;
}
输出结果:
0
0
0
0
0
0
0
0
0
0
程序说明:指向栈数据的指针,函数返回后,局部变量的数组不存在,会造成野指针的错误
#include <stdio.h>
int* g()
{
int a[10] = {0};
return a;
}
void f()
{
int i = 0;
int b[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* pointer = g();
for(i=0; i<10; i++)
{
printf("%d\n", pointer[i]);
}
}
void main()
{
f();
return 0;
}
输出结果:
0
-1217224704
0
0
-1074600104
-1218653521
-1217221952
134514048
-1074600172
-1218653568
总结:活动记录销毁后,原先活动记录里面的变量也就被销毁,因此局部变量的地址也就没有意义了,所以会出现野指针。
7. 程序中的堆
问题:如果说在程序中需要一段额外的空间来完成任务,比如数据结构中经常为了效率,用空间换时间,这个时候我们临时的需要一段空间,该怎么办?——动态内存分配。
动态内存分配是分配堆中的内存。
- 堆是程序中一块预留的内存空间,为了是给程序自由使用。如我们临时需要一段内存空间,可以使用malloc来在堆内存中取一块来使用。
- 堆中的内存需要主动的返还,因此堆中被程序申请使用的内存在被主动释放前将一直有效。
- 如果只申请使用堆空间而不返还,会导致堆空间使用完毕, 程序会越运行越慢,最后导致程序无法运行。
问题: 栈的作用是什么?为什么有了栈还需要堆空间?
栈的作用是为了函数调用。
在程序运行过程中,需要临时的一段内存空间,因此需要堆空间来获取一段临时的内存空间。
- C语言程序中通过库函数的调用获得堆空间
头文件:malloc.h
malloc:以字节的方式动态申请堆空间
free:将堆空间归还给系统
系统对堆空间的管理方式——空闲链表法,位图法,对象池法等等。
空闲链表法:

C语言是以高效而闻名的,因此malloc调用函数向堆内存申请空间时也必须是高效的。系统将堆中的内存组织成一个链表,如上图所示,图中每个节点的数字为对应节点之下的每个单元的内存大小是多少。如12Bytes表示其下方内存每个单元的大小为12Byte。当程序调用malloc函数之后,系统就会遍历这个空闲链表,查找malloc所需要的内存大小跟哪一个节点数的内存大小最接近。如上图中,申请int类型,即4个字节的大小,遍历空闲链表后发现跟5Bytes节点最为接近,于是将会在其节点下方的内存中寻找一个可用的单元,找到后将单元地址返回给p指针。所以说,malloc这个函数返回的可用空间可能会比申请的空间大,是因为系统通过空闲链表管理堆内存时,它会找malloc申请的最接近的那一个节点下对应的堆内存。
8.程序中的静态存储区
静态存储区随着程序的运行而分配,随着程序的结束而结束。也就是说静态存储区在程序运行的那一刻就被系统给分配出来了,因此,程序在编译期的时候就已经知道静态存储区的大小,运行的时候仅仅给静态存储区分配空间,且在运行期静态存储区的大小是不能改变的。静态存储区主要是用于保存全局变量和静态局部变量。静态存储区的信息最终会保存在可执行程序中,即静态存储区的大小信息、开始信息、结束信息等保存在.exe或.out可执行文件中。
静态存储区的验证
#include <stdio.h>
int g_v = 1;
static int g_vs = 2;
void f()
{
static int g_vl = 3;
printf("&g_vl = %p\n", &g_vl);
}
int main()
{
printf("&g_v = %p\n", &g_v);
printf("&g_vs = %p\n", &g_vs);
f();
return 0;
}
输出结果:
&g_v = 0x804a020
&g_vs = 0x804a024
&g_vl = 0x804a028
总结:在内存空间中,g_v为全局变量,g_vl为静态全局变量,g_vs为静态局部变量,这三个不同的变量存放在一段连续的存储空间中(顺序存放),因为这个三个变量都是存放在静态存储区的。
9.小结
- 栈、堆和静态存储区是程序中的三个基本数据区
- 栈的主要作用是为了调用函数的时候维护对应的活动记录;
- 堆主要是用于内存的动态申请和归还;
- 静态存储区用于保存全局变量和静态变量。


