C语言数据存储区主要分为栈、堆、静态存储区三个。
1.程序中的栈
栈在程序中用于维护函数调用上下文,且函数中的参数和局部变量存储在栈上。可以这么认为,没了栈的话,程序几乎无法运行,这里的栈和数据结构里的栈是一个概念,但表现形式不一样。栈遵循后进先出原则,栈是一种后进先出的行为。
补充一下
(1)ebp是当前函数的存取指针,即存储或者读取数时的指针基地址。
(2)esp是当前函数的栈顶指针。
每一次发生函数的调用(主函数调用子函数)时,在被调用函数初始时,都会把当前函数(主函数main)的EBP压栈,以便从子函数返回到主函数时能够获取EBP。ESP就是一直指向栈顶的指针,而EBP仅仅是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数參数、局部变量等。
将内存看成是一个栈:

为什么说函数调用的时候,少不了栈呢?
这是因为,在调用函数的时候,需要去维护一个活动记录,如下图所示,栈保存了一个函数调用所需的维护信息,包括了参数,返回地址,局部变量,调用上下文等,其他数据信息指的是临时变量等。

函数调用过程
每次函数调用都对应着一个栈上的活动记录,调用函数的活动记录位于栈的中部,被调函数的活动记录位于栈的顶部。

如上图,main函数中调用其他函数,先要将main函数压栈,再调用函数f,函数f里有函数g,那么函数f也被压栈,栈顶指针指向的为函数g。
函数调用的栈变化1
从main函数开始运行,栈就有了main函数的活动记录

函数调用的栈变化2
main函数调用函数f,在栈上又建立了函数f的活动记录。
栈顶指针发生了移动,函数f活动记录中的返回地址其实就是没调用函数f前main函数栈顶指针的地址,old ebp保存的是上一次ebp指针保存的位置。
新老活动记录通过ebp指针来关联,ebp指针向上读四个字节,就读取到了栈顶指针需要返回的地址,所以说ebp指针的作用就是进行函数调用的返回。

函数调用的栈变化3
当从函数f返回主函数main
就通过ebp指针获取返回地址给栈顶指针,栈顶指针就指向了之前的位置,然后可以继续执行下一次的函数调用。

到这里我们可以发现到,栈空间里的数据,不会由于函数的返回而立即改变,函数返回仅仅是修改了ebp指针和esp指针的地址值,并没有去改变栈空间里面的数据,所以我们千万不要返回函数内部的局部变量的地址,也不要返回局部数组,这是因为栈空间里面的数据不会由于函数的返回而改变。如果我们在返回main函数之后,又调用了其他的函数,那么之前保存的函数f的活动记录就会被另一个函数使用了,这一部分内容就会发生变化。所以返回一个局部变量地址或者局部数组是没有意义的,因为已经不存在了,这样就会造成野指针的错误,而造成程序运行的崩溃。
函数调用栈上的数据
函数调用时,对应的栈空间在函数返回前是专用的;
函数调用结束后,栈空间将被释放,数据不再有效。

程序来说明
#include <stdio.h>
int* g()
{
int a[10] = {0};
return a;
}
void f()
{
int i = 0;
int b[10] = {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\r\n",b[i]);
}
}
int main()
{
f();
return 0;
}
编译运行,结果却是发生了段错误。注意给出的告警信息,就是那里出了问题,返回了局部变量的地址。函数f还调用了printf打印函数,把函数g的活动记录给清除了,原来的那些数组就被销毁了,没有意义了,这就是所谓的野指针。

为什么有了栈还需要堆?
如果我们确实需要一块空间来完成程序,例如在数据结构中采用空间换时间,提高效率那么就需要开辟一段新空间。
栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如∶局部数组
2.程序中的堆
概念:堆是程序中一块预留的内存空间,可由程序自由使用,堆中被程序申请使用的内存在被主动释放前将一直有效。C语言程序中通过库函数的调用获得堆空间
- 头文件:malloc.h
- malloc :以字节的方式动态申请堆空间
- free :将堆空间归还给系统
不同的系统西面,对堆的管理方式是不一样的,系统对堆空间的管理方式主要有三种:空闲链表法,位图法,对象池法等等。
以空闲链表法为例,C语言以高效原则来设计,向堆区申请内存的时候也要高效,系统就会将堆空间组织成一个链表,方框表示的是每个节点的内存空间的大小,例如我们要申请4字节的内存,而5字节内存的节点被申请完了,那么就会到最近的12字节节点来申请内存空间。

3.程序中的静态存储区
- 静态存储区随着程序的运行而分配空间
- 静态存储区的生命周期直到程序运行结束
- 在程序的编译期静态存储区的大小就已经确定
- 静态存储区主要用于保存全局变量和静态局部变量
- 静态存储区的信息最终会保存到可执行程序中
静态存储区的验证:
#include <stdio.h>
int g_v = 1;
static int g_vs = 2;
void f()
{
static int g_vl = 3;
printf("%p\n", &g_vl);
}
int main()
{
printf("%p\n", &g_v);
printf("%p\n", &g_vs);
f();
return 0;
}
编译运行结果如下,可以发现内存空间是连续的,以四字节之差连续存储三个变量。

4.小结
- 栈,堆和静态存储区是程序中的三个基本数据区
- 栈区主要用于函数调用的使用
- 堆区主要是用于内存的动态申请和归还
- 静态存储区用于保存全局变量和静态变量
C语言数据存储主要涉及栈、堆和静态存储区。栈用于函数调用,保存参数、局部变量和返回地址;堆提供动态内存分配,适用于需要在函数外部保留的数据;静态存储区存放全局变量和静态局部变量,其生命周期贯穿程序始终。函数调用过程中,栈通过ebp和esp指针管理,避免野指针错误。堆管理通过malloc和free函数,不同系统采用不同管理策略。
1163





