39_程序中的三国天下

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.小结

  • 栈、堆和静态存储区是程序中的三个基本数据区
  • 栈的主要作用是为了调用函数的时候维护对应的活动记录;
  • 堆主要是用于内存的动态申请和归还;
  • 静态存储区用于保存全局变量和静态变量。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容