• 【C/C++】优雅而具体地向学生解释栈空间的分配与利用


    冯·诺依曼指着下方巨大的人列回路开始介绍:“陛下,… ;外围整齐的部分是内存,构建到这部分时我们发现人手不够,好在这部分每个单元的动作最简单,就训练每个士兵拿多种颜色的旗帜,组合起来后,一个人就能同时完成最初二十个人的操作,这就使内存容量达到了运行‘秦1.0’操作系统的最低要求。”

    ——刘慈欣《三体》

    本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔
    叔";本文不允许以纸质及电子出版为目的进行抄摘或改编。
    1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
    2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
    3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频

    到目前为止,本书示例代码中定义的变量大多是自动变量(automatic variables)。比如下述程序中的a和f。

    int dummy() {
        int a = 3;
        float f = 1.2;
        //...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    所有对象都会占据内存空间,自动变量所占据的内存空间的分配与回收是由编译器负责的。而容纳这些自动变量的,是一种被称为堆栈(stack),简称为栈的数据结构。据信本书的大部分读者都不具备操作系统、CPU体系结构方面的知识,因此我们只能以比较“形象”但可能不够准确的方式来解释编译器是如何借助于栈来管理自动变量的。

    应用程序的栈空间是由操作系统分配的“固定”的有限大小的内存空间,如图8-1所示,这个栈空间是由栈底开始的一段连续的内存。在多数计算机上,栈空间由高地址的栈底向低地址方向生长。除了栈底之外,计算机还会保存一个叫做栈顶指针(stack pointer)的地址,从栈底一直到栈顶指针之间的区域,表示已经被分配利用的栈空间(网格状区域),栈顶指针之下的空白区域,则表示尚未利用的栈空间。
    图8-1 栈
    我们结合图8-1来理解下述“伪代码”执行过程中的栈空间利用行为:

    void func(int b1, float b2, double b3){
        double b4 = b1 * b2 + b3;
        //...
    }
     
    int main() {
        int a1 = 1;
        float a2 = 2.1F;
        func(a1,a2,15.7);
        //...
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    🚩第7 ~ 8行:编译器为自动变量a1,a2分配内存,它们存储在A区域,见图8-1函数调用前(1)。

    🚩第9行:调用执行函数func(),函数的形参b1、b2、b3以及func()内的局部变量b4都需要占据内存空间,所以编译器生成的机器指令将栈顶指针下移,并将上述自动变量分配在B区域,见图8-1函数调用时(2)。需要说明的是,函数调用过程中还有一些额外的内存消耗,这些内存消耗也会使用图中B区域的空间。

    🚩第4行:当func()函数执行结束后,其形参b1、b2、b3以及局部变量超出作用域范围。此时,编译器生成的机器指令将栈顶指针住上移,以回收栈空间。请参见图8-1函数调用返回后(3)。此时,B区域成为未利用的栈空间的一部分,可供后续程序使用。

    不太严谨地,我们可以按如下方式总结C/C++语言的自动变量内存管理:当一个新的代码块(比如函数调用)存在新定义的自动变量时,编译器生成代码下移栈顶指针,将新定义的自动变量安排在新开发的栈区域内。因代码块执行结束导致局部的自动变量超出作用范围时,编译器生成代码上移栈顶指针,回收内存。整个程序的执行过程伴随着无数次的栈顶指针移动,栈空间反复地生长和收缩。

    我们通过执行下述C语言程序来“印证”上述理论的正确:

    //Project - StackExample2
    #include 
    
    void dummy(int i, int j){
        int k = i + j;
        printf("---------------dummy-------------\n");
        printf("&i = %p, &j = %p\n", &i, &j);
        printf("&k = %p, k = %d\n",&k, k);
    }
    
    void add(int d, int e, int f){
        int g = d + e;
        int h = f;
        printf("---------------add---------------\n");
        printf("&d = %p, &e = %p, &f = %p\n", &d, &e, &f);
        printf("&g = %p, &h = %p, sum = %d\n", &g, &h, g + h);
    }
    
    int main() {
        int a=1,b=1,c=1;
        printf("&a = %p, &b = %p, &c = %p\n", &a, &b, &c);
        add(a,b,c);
        dummy(a,b);
        add(a,b,c);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    上述代码的执行结果为:

    &a = 000000000061FE1C, &b = 000000000061FE18, &c = 000000000061FE14
    ---------------add---------------
    &d = 000000000061FDF0, &e = 000000000061FDF8, &f = 000000000061FE00
    &g = 000000000061FDDC, &h = 000000000061FDD8, sum = 3
    ---------------dummy-------------
    &i = 000000000061FDF0, &j = 000000000061FDF8
    &k = 000000000061FDDC, k = 2
    ---------------add---------------
    &d = 000000000061FDF0, &e = 000000000061FDF8, &f = 000000000061FE00
    &g = 000000000061FDDC, &h = 000000000061FDD8, sum = 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    说明:在读者的计算机上,执行结果中的地址很可能与本书不同。

    🚩第20 ~ 21行:输出main()函数内局部自动变量a、b、c的地址。从执行结果的第1行可见,它们位于较高的地址,比较接近栈底。

    🚩第22行:执行add()函数。从执行结果的第2 ~ 4行可见,add()的形参d、e、f及局部变量g、h的地址较a、b、c更小,这说明栈顶指针下移,d、e、f、g、h被分配在了新开辟的栈区域内。

    🚩第23行:在add()函数调用返回后执行dummy()函数。从执行结果的第5 ~ 7行可见,dummy()函数的形参i、j以及局部变量k被分配了与add()函数形参及局部变量“几乎”相同的内存地址。这提示add()函数返回时,编译器向上移动栈顶指针回收了内存,调用dummy()函数时又再次下移栈顶指针,上述被回收的内存被再次利用。

    🚩第24行:在dummy()函数调用返回后再次执行add()函数。从执行结果的第8 ~ 10行可见,第二次add()函数调用时,其内存分配情况与第一次add()函数调用完全一致。这提示,编译器借助于栈,“完美”地处理了自动变量的内存分配和回收,坏账率为0,借出去多少,就收回来多少。

    为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!

    简洁的C及C++
    由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
    Python编程基础及应用
    由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造

    如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。

    Python编程基础及应用

    Python编程基础及应用实验教程
    在这里插入图片描述

  • 相关阅读:
    SSH远程登录协议
    葡萄糖-聚乙二醇-链霉亲和素|Streptavidins-PEG-Glucose|链霉亲和素-PEG-葡萄糖
    springMVC/spring/Mybatis三大框架整合 看完必懂 秒杀阿里面试官
    Cargo 使用教程
    【解决方案】Ubuntu设置Matlab桌面启动快捷方式
    PMP考试300条知识点汇总,20天上岸!
    算法通关村第12关【白银】| 字符串经典问题
    通用游戏服务器架构设计
    Spring Ioc源码分析系列--自动注入循环依赖的处理
    【redis的基本数据类型】
  • 原文地址:https://blog.csdn.net/SeaBiscuitUncle/article/details/126568488