【C语言进阶剖析】39、栈、堆和静态存储区

1 程序中的栈

  • 栈在程序中用于维护函数调用的上下文
  • 函数中的参数和局部变量存储在栈上

在这里插入图片描述
栈保存了一个函数调用所需的维护信息,包括,函数参数,函数返回地址,局部变量,调用上下文,分布如下:
在这里插入图片描述

push 操作相当于往栈中填数据,esp指针会向下走。pop操作相当于将栈顶数据弹出,esp指针会网上走。

函数调用时,首先向栈中压入函数的参数,再压入函数的返回地址。esp 寄存器指向栈顶指针,ebp 寄存器指向函数的返回地址。

1.1 函数调用过程

每次函数调用都对应着一个栈上的活动记录

  • 调用函数的活动记录位于栈的中部
  • 被调用函数的活动记录位于栈的顶部

分布如下:
在这里插入图片描述

1.2 函数调用的栈变化

1、从 main() 开始执行

  • main 函数开始执行时,首先入栈的是函数参数,然后是函数的返回地址
  • old_ebp 代表调用 main 函数的ebp的位置。这个暂且不管
  • ebp 是函数栈帧,用于定位查找其他参数。ebp 向上偏移 4 字节(在X86 32位系统中,栈是以4字节为单位进行存储数据)就能找到返回地址(这个返回地址是调用 main 函数的那个函数之前执行指令的地址)。ebp 向下偏移4字节就是 old_ebp。
  • esp是栈顶指针,指向栈顶

2、当 main() 调用 f()

  • 函数 f() 的参数先入栈,然后函数返回地址入栈(用这个地址可以返回到 main 函数调用 f() 的位置),然后 main 函数的 ebp 的地址入栈,最后是是函数中的局部非静态变量信息入栈。
  • 此时的 ebp 向上偏移 4 个字节找到 f() 的返回地址,向下偏移 4 字节,找到 old_ebp,这个 old_ebp 指向 main() 函数开始执行时,ebp 指向的位置。

3、当从 f() 调用中返回到 main()

  • 通过 f() 函数栈帧中的 old_ebp 找到 main 函数的 ebp,然后将当前 ebp 寄存器指向它。
  • 通过 f() 栈帧中的返回地址找到 main 函数之前被中断的地址处,main函数继续执行。

注意:当 f() 返回后,原本是 f() 栈中的数据就不在再维护,内容也不会自动改变,但是如果此时 main() 函数又调用了其他函数,原本是 f() 栈中的数据就会被覆盖,这也就是不能在函数外使用函数非静态局部变量的原因,因为内存中数据可能被改变了。

在这里插入图片描述

1.3 函数调用栈上的数据

  • 函数调用时,对应的栈空间在函数返回前是专用的
  • 函数调用结束后,栈空间将释放,数据不再有效

举例如下:
在这里插入图片描述
g() 函数返回后,g() 函数的栈空间将被释放,数据不再被维护,但是如果这部分空间没有被使用,数据不会改变,如果调用了其他函数,这部分空间将被其他函数使用,数据将改变。
下面我们来验证一下:

// 39-1.c
#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();
    // printf("%p\n", pointer);
    for (i = 0; i < 10; i++)
    {
        b[i] = pointer[i];
    }
    for (i = 0; i < 10; i++)
    {
        printf("%d\n",b[i]);
    }
}
int main()
{
    f();
    return 0;
}

我们定义了数组 b,然后将已经不再有效的 g() 中的数据复制到数组 b 中。

$ gcc 39-1.c -o 39-1
39-1.c: In function ‘g’:
39-1.c:6: warning: function returns address of local variable
$ ./39-1
0
0
0
0
0
0
0
0
0
0

从结果看,虽然函数 g() 的 栈空间已经不再有效了,但是此时没有调用其它函数,原来的数据没有改变。
现在将源代码第 13 行不再注释,重新编译运行,如下。

$ gcc 39-1.c -o 39-1
39-1.c: In function ‘g’:
39-1.c:6: warning: function returns address of local variable
delphi@delphi-vm:~/pra$ ./39-1
0xbfed36f8
-1074972904
5947380
0
-1074972824
-1074972888
4828192
-1074972808
4828192
5948640
134514080

在 g() 函数返回之后,我们调用了 printf 函数,导致原 g() 函数的栈空间被占用了,重新写入数据,所以数据发生改变。

2 程序中的堆

  • 堆是程序中的一块预留的空间,可由程序自由使用
  • 堆中被程序申请使用的内存在被主动释放前将一直有效

为什么有了栈还需要堆呢?
因为栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如局部变量。

C 语言程序中通过库函数得调用获得堆空间。头文件为 malloc.h, malloc 以字节得方式动态申请堆空间,free 将堆空间归还给系统

系统对堆空间得管理方式有

  • 空闲链表法,位图法,对象池法等等

下面介绍一下空闲链表法,空闲链表就是操作系统将整个可用的堆内存空间分为一块一块的,将多个内存块串联成一个链表形式。维护记录已分配内存段和空闲内存段的链表,其中链表中的每一个结点包含一个进程或者两个进程间的一个空闲区域。从图中可以看到内存布局,链表中的每一个结点都包含以下域:空闲区(H)或进程(P)的指示标志、起始地址、长度和指向下一节点的指针。
在这里插入图片描述
在本例中,段链表是按照地址排序的,其好处是当进程终止或者被换出时链表的更新非常直接。一个要中终止的进程一般有两个邻居,如下图所示,他们可能是进程也可能是空闲区,就导致了下图的四种组合。a) 中将 P(进程)替换为H(空闲区),b)和c)中两个结点被合并成一个结点,从链表中删除一个结点。在 d)中三个结点被合并为一个,从链表中删除两个结点。

在这里插入图片描述
因为进程表中表示终止进程的结点中通常含有指向对应于其段链表结点的指针,因此段链表使用双链表可能要比单链表更方便。这样的结构更易于找到上一个结点,并检查是否可以合并。

当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以用来为创建的进程(或从磁盘换入的已存在的进程)分配内存。这里,假设存储管理器知道要为进程分配的多大的内存。

  • 首次适配(first fit)算法。存储管理器沿着段链表进行搜索,直到找到一个足够大的空闲区,除非空闲区大小和要分配的空间大小正好一样,否则将该空闲区分为两部分,一部分供进程使用,另一部分形成新的空闲区。首次适配算法是一种速度很快的算法,因为它尽可能少地搜索链表结点。
  • 下次适配(next fit)算法。它的工作方式和首次适配算法相同,不同点是每次找到合适的空闲区时都记录当时的位置。以便在下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次适配算法那样每次都从头开始。下次适配算法的性能略低于首次适配算法。
  • 最佳适配(best fit)算法。搜索整个链表(从开始到结束),找出能够容纳进程的最小的空闲区。试图找出最接近实际需要的空闲区,以最好地区配请求和可用空闲区,而不是先拆分一个以后可能会用到的大的空闲区。
  • 最差适配(worst fit)算法,由于最佳适配的空闲区会分裂出很多非常小的空闲区,为了避免这一问题,可以考虑总是分配最大的可用空闲区,使新的空闲区比较大从而可以继续使用。
  • 快速适配(quick fit)算法,它为那些常用大小的空闲区维护单独的链表。例如,有一个 n 项的表,该表的第一项是指向大小为 4KB 的空闲区链表表头的指针,第二项是指向大小为 8KB 的空闲区链表表头的指针,第三项是指向大小为 12KB 的空闲区链表表头的指针,以此类推。快速适配算法寻找一个指定大小的空闲区是十分快速的,但是在一个进程终止或被换出时,寻找相邻块,查看是否可以合并的过程非常费时。如果不进行合并,内存将会很快分裂出进程无法利用的小空闲区。

3 程序中的静态存储区

  • 静态存储区随着程序的运行而分配空间
  • 静态存储区的生命周期直到程序运行结束
  • 在程序的编译期静态存储区的大小就已经确定
  • 静态存储区主要用于保存全局变量和静态局部变量
  • 静态存储区的信息最终会保存到可执行程序中

下面看一个实例:静态存储区的验证:

// 39-2.c
#include<stdio.h>
int g_v = 1;
static int g_vs = 2;
void f()
{
    static int g_v1 = 3;
    printf("%p\n", &g_v1);
}
int main()
{
    printf("%p\n", &g_v);
    printf("%p\n", &g_vs);
    f();
    return 0;
}
$ gcc 39-2.c -o 39-2
$ ./39-2
0x563e28677010
0x563e28677014
0x563e28677018

可以看到,三个变量分别是全局变量,静态全局变量和静态局部变量,他们的地址是相同的,他们都位于静态存储区。

我们再看一段程序,并分析,程序中的数据位于哪个位置。
在这里插入图片描述

  • .text:以编译程序的机械代码。
  • .data :已初始化的全局变量和静态 C 变量
  • .bss :未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量,在目标文件中,这个节不占据实际的空间,他仅仅是一个占位符,这是为了空间效率。

在这里插入图片描述
所以,静态存储区实际就是.data与.bss区域

4 小结

栈,堆和静态存储区是程序中的三个基本数据区

  • 栈主要用于函数调用的使用
  • 堆区主要是用于内存的动态申请和归还
  • 静态存储区用于保存全局变量个静态变量
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值