C语言(20250722)

C语言

内容提要

  • 函数
    • 函数的概述
    • 函数的分类
    • 函数的定义
    • 形参和实参
    • 函数的返回值
    • 函数的调用
    • 函数的声明

函数

函数的概述

  • **函数:**实现一定功能的,独立的代码模块,对于函数的使用,一定是先定义,后使用。

  • 使用函数的优势:

    ① 我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码量。

    ② 借助函数可以减少重复性的代码。

    ③ 实现结构化(模块化:C语言中的模块化其实就是多文件+函数)程序设计思想。

    关于结构化设计思想:将大型的任务功能划分为相互独立的小型的任务模块来设计(多文件 + 函数)

  • 函数的作用:

    (1) 代码复用:避免重复编写相同功能的代码。

    (2) 模块化设计:将复杂程序拆分成多个小功能模块,每个函数负责一个独立任务,使代码逻辑结构更加清晰。

    (3) 便于维护和调试:单个函数功能单一,出现问题容易定位和修改,不需要改动整个程序。

    (4) 提高开发效率:便于多人协同开发时,分工明确,编写不同函数,最终组合成完整程序

  • 函数是C语言程序的基本组成单元:

    C语言程序必须包含一个main函数,可以包含零个或多个其他函数。

函数的分类

  • 按来源分

    • **库函数:**C语言标准库实现的并提供使用的函数,如:scanf()、- printf()、fgets()、fputs()、strlen()...

    • 自定义函数:需要程序员自行实现,开发中大部分函数都是自定义函数。

  • 按参数分:

    • **无参函数:**函数调用时,无需传递参数,可有可无返回值,如:show_all();

    • **有参函数:**函数调用时,需要参数传递数据,经常需要配套返回值来使用,如:printf("%d\n", 12);

  • 按返回值分:

    • **有返回值函数:**函数执行后返回一个值,如: if (scanf("%d", &num) != 1)

    • **无返回值函数(void):**函数仅执行操作,不返回值

  • 从函数调用的角度分:

    • **主调函数:**主动去调用其他函数的函数。(main函数只能作为主调函数)

    • **被调函数:**被其他函数调用的函数。

    • 举例:

      c 复制代码
      // 主调函数
      int main()
      {
          // 被调函数
          printf("hello world!\n");
      }

      注意:很多时候,尤其是对于自定义函数,一个函数有可能既是主调函数,又是被调函数。

      c 复制代码
      int fun_b()
      {
          printf("函数B\n");
      }
      // fun_a是主调函数
      int fun_a()
      {
          printf("函数A\n");
          // fun_b是背调函数
          fun_b();
      }
      int main()
      {
          // fun_a是被调函数
          fun_a();
      }

      以上案例中,fun_a()相对fun_b()来说是主调函数,同时对于main()函数来说,他又是被调函数。

函数的定义

定义

语法:

c 复制代码
[返回类型] 函数名([形参列表])      --函数头 | 函数首部
{
    函数体语句;				   --函数体,整个{}包裹的内容都属于函数体,函数体的{}不能省略
}

函数头:

  • **返回类型:**函数返回值的类型
  • **函数名:**函数的名称,遵循标识符的命名)(不能以数字开头,只能包含大小写字母、下划线和数字,建议:小写 + 下划线,举例:show_all(),或者小驼峰命名法,第一个单词首字母小写,其他单词首字母大写,举例:showAll()
  • **形参列表:**用于接收主调函数传递的数据,如果有多个参数,使用分隔,切每一个形参都需要指明类型。

小贴士:

① 形参:主调函数给被调函数传递数据:主调函数 → 被调函数

② 返回值:被调函数给主调函数返回数据:被调函数 → 主调函数

通过生活中的案例理解函数调用:

假设:饮料店的工作人员通过榨汁机榨取新鲜果汁

理解:

工作人员:主调函数

榨汁机: 被调函数

水果: 传递的参数

果汁: 函数的返回值

工作人员向榨汁机放入一个水果:主调函数调用被调函数,并传递数据

工作人员用杯子接收榨汁机榨出的果汁:主调函数接收被调函数返回的数据

说明:

  • 函数的返回值:就是返回值的类型,两个类型可以不同,但是必须能够进行转换,举例:

    c 复制代码
    double fun_a() // 函数的返回类型是:double
    {
        return 12; // 函数的返回值是:int
    }
    // 分析:此时需要转换,函数在执行的时候,会自动提升int的类型为double,此时属于隐式转换,正常转换,以上正确
    c 复制代码
    int[] fun_b() // 函数的返回类型是:int[]
    {
        return 12; // 函数的返回值是:int
    }
    // 分析:此时需要提升int的类型为int[],int不能转换为int[],以上错误!
    
    
    int fun_c() // 函数的返回类型是:int
    {
        return 12.5;// 函数的返回值是:double
    }
    // 分析:此时需要将double类型转换为int类型,浮点型转整型,会丢失小数部分,保留整数位,以上正确

    大家可以这样理解(非官方):

    大家可以将函数的返回类型理解为变量的类型,将函数的返回值理解为变量的值。

范例:

c 复制代码
#include <stdio.h>
double fun_a()
{
    return 12;// 就是将int类型的12赋值给double类型的匿名变量 int -->
    double
}
int fun_b()
{
    return 12.5;// 就是将double类型的12.5赋值给int类型的匿名变量
    double --> int 此时会舍弃掉小数部分
}
double fun_c()
{
    return 12.5; // 就是将double类型的12.5赋值给double类型的匿名变量
    double --> double
}
int main(int argc,char *argv[])
{
    // 接收函数返回值,函数返回什么类型,就用什么类型接收
    double result1 = fun_a();// 主调函数使用double来接收被调函数返回
    的double,double --> double
        printf("%lf\n",result1);
    int result2 = fun_b(); // 主调函数使用int来接收被调函数返回的
    int,int --> int
        printf("%d\n",result2);
    int result3 = (int)fun_c(); // 主调函数使用int来接收被调函数返回
    的double,int --> (int)double
        printf("%d\n",result3);
    return 0;
}
  • 在C语言中无返回值时应明确使用void类型(空类型/无类型),举例:

    c 复制代码
    void test() // 此时这个函数,没有返回值,如果需要提前结束函数,写法:
        return;
    {
        printf("hello world!\n");
    }
    // 下面写法等价于上面写法
    void test()
    {
        printf("hello world!\n");
        return; // 一般,这个return;省略不写
    }
  • 在C语言中,C89标准允许函数的返回类型标识符可以省略,如果省略,默认返回int。C99/C11标准要求必须明确指定返回类型,不再支持默认int类型,举例:

    c 复制代码
    // 写法1:(C89标准),main的返回类型是int类型,默认返回值是0,等价于写法2 不推荐
    main()
    {
        ...
    }
    // 写法2:(C99后推荐),等价于上面写法
    int main()
    {
        return 0;
    }
  • 函数中返回语句的形式为return(表达式)或者return 表达式

    c 复制代码
    // 写法1
    int main()
    {
        return(0);
    }
    // 写法2
    int main()
    {
        return 0;
    }
  • 如果参数列表中有多个参数,则它们之间要用分隔;即使他们类型相同,在形式参数中只能逐个进行说民,举例:

    c 复制代码
    // 正确举例
    int avg(int x, int y, int z)
    {
        ...
    }
    // 错误举例
    int avg(int x, y, z)
    {
        ...
    }
  • 如果参数列表中没有参数,我们可以不写,也可以用void标识,举例:

    c 复制代码
    // 写法1:推荐
    int main()
    {
        ...
    }
    // 写法2:
    int main(void)
    {
        ...
    }
  • C89开始,提供了变长参数,也就是一个函数的参数个数可以是不确定的。需要引入<stdarg.h> ,扩展:

    c 复制代码
    [返回类型] 函数名(参数列表, ...)
    {
        ...
    }

    举例:

    c 复制代码
    #include <stdio.h>
    #include <stdarg.h>
    // 计算n个整数的平均值
    double average(int n, ...) { // ...只能放在 具体的参数列表的后面
        va_list args; // 声明参数列表对象
        int sum = 0;
        va_start(args, n); // 初始化参数列表,n是最后一个固定参数
        // 遍历所有可变参数
        for (int i = 0; i < n; i++) {
            // 获取一个int类型的参数
            sum += va_arg(args, int);
        }
        va_end(args); // 清理参数列表
        return (double)sum / n;
    }
    int main() {
        printf("平均值: %.2f\n", average(3, 10, 20, 30)); // 20.00
        printf("平均值: %.2f\n", average(5, 1, 2, 3, 4, 5)); // 3.00
        return 0;
    }
案例
案例1
  • 需求:计算1~n之间自然数的阶乘值

  • 代码:

    c 复制代码
    #include <stdio.h>
    /**
    * 定义一个函数,实现1~n之间的阶乘计算
    * @param n:阶乘上限
    * @return n的阶乘值
    */
    size_t fun_1(int n)
    {
        int i; // 循环变量
        size_t s = 1; // 阶乘值,初始值是1
        for (i = 1; i <= n; i++) s *= i;
        return s;
    }
    int main(int argc,char *argv[])
    {
        printf("1~12的阶乘结果是:%lu\n", fun_1(12));
        printf("1~20的阶乘结果是:%lu\n", fun_1(20));
        printf("1~30的阶乘结果是:%lu\n", fun_1(30));
        printf("1~40的阶乘结果是:%lu\n", fun_1(40));
        return 0;
    }

    运行结果:

​ 注意:这里计算结果为0,是因为数据太大,超过int存储范围,高位数据丢 失,低位数据转出来为0,建议使用unsigned long类型。

案例2
  • 需求:计算一个圆台两个面的面积之和。
  • 代码:

    c 复制代码
    #include <stdio.h>
    #include <math.h>
    #define PI 3.1415926
    /**
    * 定义一个函数:实现圆的面积计算
    * @param r:圆的半径
    * @return 圆的面积
    */
    double cicle_area(double r)
    {
        // return PI * r * r;
        return PI * pow(r,2.0); // pow(底数,指数);编译需要加 -lm
    }
    int main(int argc,char *argv[])
    {
        // 定义两个半径,两个面积
        double r1,r2,area1,area2;
        printf("请输入两个圆的半径:\n");
        scanf("%lf,%lf", &r1, &r2);
        // 计算面积
        area1 = cicle_area(r1);
        area2 = cicle_area(r2);
        printf("一个圆台两个面的面积之和是%lf\n", area1 + area2);
        return 0;
    }
  • 编译命令

    c 复制代码
    gcc demo03.c -lm

形参和实参

形参(形式参数)
定义

函数定义时指定的参数,形参是用来接收数据的。函数定义时,系统不会为形参申请内存,只有当函数调用时,系统才会为形参申请内存。主要用于存储实际参数,并且当函数返回时(执行return),系统会自动回收为形参申请的内存资源。

  • C语言中所有的参数传递都是值传递

  • 若要修改实参,需要传递指针,指针传递本质上也是值传递(后续章节讲)。

案例
  • 需求:判断一个数是偶数还是奇数

  • 代码:

    c 复制代码
    #include <stdio.h>
    /**
    * 方式1
    */
    void fun1(int n) // 这里的n就是形参
    {
        if (n % 2 == 0)
        {
            printf("%d是偶数!\n", n);
            return; // 提前结束函数,后续代码不再执行
        }
        printf("%d是奇数!\n", n);
    }
    /**
    * 方式2
    */
    int fun2(int n)
    {
        if (n % 2 == 0)
        {
            printf("%d是偶数!\n", n);
            return -1;
        }
        printf("%d是奇数!\n", n);
        return 0;
    }
    int main(int argc,char *argv[])
    {
        fun1(5);
        fun2(5);
        return 0;
    }
实参(实际参数)
定义

实参是函数调用时由主调函数传递给被调函数的具体的数据。实参可以是常量、变量、表达式、带有返回值的函数等。

关键特性

  1. 类型多样性:

    • 实参可以是常量、变量或者表达式...。

    • 例如:

      c 复制代码
      fun(12); // 常量作为实参
      fun(a); // 变量作为实参
      fun(a + 12); // 表达式作为实参
      fun(func()); // 带有返回值的函数作为实参
  2. 类型转换:

    • 当实参和形参类型不同时,会按照赋值规则进行类型转换。

    • 类型转换可能导致精度丢失。

    • 例如:

      c 复制代码
      #include <stdio.h>
      /**
      * 求一个数的绝对值
      */
      double fabs(double a)
      {
          return a < 0 ? -a : a;
      }
      int main()
      {
          int x = 12, y = -12;
          int x1 = (int)fabs(x); // x会被隐式转换为double,fabs返回的是double类型数据
          int y1 = (int)fabs(y);
      }

      注意:函数调用的时候,通过实参给形参赋值。形参类似于变量,实参类似于变量的值。

      函数调用时:

      主调函数通过实参给被调函数的形参赋值,可理解为:将主调函数的值赋值给被调函数的变量。

      函数返回时:

      被调函数通过返回值给主调函数赋值,可理解为:将被调函数的值赋值给主调函数的变量。

  3. 单向值传递:

    • C语言采用单向值传递机制(赋值的方向:实参 → 形参)

    • 实参仅将其值赋值给形参,不传递实参本身。

    • 形参值的改变不会影响实参。

    • 案例:

      c 复制代码
      int modify(int n) // n的变量地址:0x11
      {
          n = 20; // 修改 0x11这个空间的数据位20 n = 20
          return n;
      }
      int main()
      {
          int n = 10; // n的变量地址:0x21
          modify(n); // 将0x21中的数据赋值给0x11这个空间
          printf("%d\n", n); // 10
      }
  4. 内存独立性

    • 实参和形参在内存中占据不同的空间

    • 形参拥有独立的内存地址

演示

c 复制代码
#include <stdio.h>
int fun(int n) // n是形参
{
    printf("形参n的值:%d\n", n);
    n += 5; // 修改形参的数据
    return n;
}
int main()
{
    int a = 10;
    printf("调用前实参a的值:%d\n", a); // 10
    // 变量作为实参
    int res = fun(a); // a 是实参
    printf("调用前实参a的值:%d\n", a); // 10
    printf("函数返回值:%d\n", res); // 15
    // 常量作为实参
    fun(12); //字面量12作为实参
    // 表达式作为实参
    fun(a + 12); // 表达式作为实参
    return 0;
}

上述示例程序会输出:

c 复制代码
调用前实参a的值: 10
形参n的值: 10
调用后实参a的值: 10
函数返回值: 15
形参n的值: 12
形参n的值: 22

案例

  • 需求:输入4个整数,要求用一个函数求出最大数。

  • 分析:

    • 设计一个函数,这个函数只求2个数的最大数

    • 多次复用这个函数实现最终的求值

    • 代码:

      c 复制代码
      #include <stdio.h>
      /**
      * 定义一个函数,求2个数的最大值
      * @param x,y:参与比较的整数
      * @return 返回最大值
      */
      int get_max(int x, int y)
      {
          return x > y ? x : y;
      }
      int main(int argc,char *argv[])
      {
          // 定义4个变量,用来接收控制台输入
          int a,b,c,d;
          // 定义一个变量,存储最大值
          int max;
          printf("请输入4个整数:\n");
          scanf("%d%d%d%d", &a, &b, &c, &d);
          // 求a,b最大值
          max = get_max(a,b);
          // 求a,b,c最大值
          max = get_max(max,c);
          // 求a,b,c,d最大值
          max = get_max(max,d);
          printf("%d,%d,%d,%d中的最大值是%d\n",a,b,c,d,max);
          return 0;
      }
    • 运行结果

函数的返回值

定义
  • 若不需要返回值,函数可以没有return语句;

    c 复制代码
    // 如果返回类型是void,return关键字可以省略
    void fun1()
    {
        ... // return;
    }
    // 这种写法,return关键字也可以省略,但是此时默认返回是 return 0
    int fun2()
    {
        ... // return 0;
    }
    // 这种写法,return关键字也可以省略,但是此时默认返回是 return 0
    fun3() // 如果不写返回类型,默认返回int,C99/C11之后不再支持省略返回类型
    {
        ... // return 0;
    }
  • 一个函数中可以有多个return语句,但是同一时刻只有一个return语句被执行。

    c 复制代码
    #include <stdio.h>
    int eq(int num)
    {
        if (num % 2 == 0) return 0;
        return 1;
    }
    int main()
    {
        int num = 5;
        printf("%d是一个%s\n", num, eq(num) == 0 ? "偶数" : "奇数"); //5是一个奇数
    }
  • 返回类型一般情况下要和函数中return语句返回的数据类型一致,如果不一致,要符合C语言中的隐式转换规则。

    c 复制代码
    double add(int a, int b) // 返回类型是double
    {
        return a + b; // 返回值的类型是int
    }
    // 简化理解: double add = a + b;
案例
  • 需求:输入两个整数,要求用一个函数求出最大值

  • 实现1:不涉及类型转换

    c 复制代码
    #include <stdio.h>
    int get_max(int x, int y)
    {
        if (x > y) return x;
        return y;
    }
    int main()
    {
        int a,b,max;
        printf("请输入两个整数:\n");
        scanf("%d%d",&a,&b);
        max = get_max(a,b);
        printf("%d,%d中的最大值是%d\n",a,b,max);
    }
  • 实现2:设计类型转换-隐式转换

    c 复制代码
    #include <stdio.h>
    double get_max(int x, int y) // int隐身转换为double
    {
        if (x > y) return x;
        return y;
    }
    int main()
    {
        int a,b,max;
        printf("请输入两个整数:\n");
        scanf("%d%d",&a,&b);
        max = (int)get_max(a,b); // 显示转换
        printf("%d,%d中的最大值是%d\n",a,b,max);
    }
  • 实现3:涉及类型转换-显示转换

    c 复制代码
    #include <stdio.h>
    int get_max(int x, int y) // 将double类型转换为int类型,可以隐式转换,也可以显示转换
    {
        double z;
        z = x > y ? x : y;
        return (int)z; // 显示转换
    }
    int main()
    {
        int a,b,max;
        printf("请输入两个整数:\n");
        scanf("%d%d",&a,&b);
        max = get_max(a,b);
        printf("%d,%d中的最大值是%d\n",a,b,max);
    }

函数的调用

调用方式

① 函数语句:

c 复制代码
test(); // 对于无返回值的函数,直接调用
int res = max(2,4); // 对于有返回值的函数,一般需要在主调函数中接收被调函数的返回值

② 函数表达式:

c 复制代码
4 + max(2,4)
scanf("%d", &num) != 1
(c = getchar()) != '\0'

③ 函数参数:

c 复制代码
printf("%d", (int)fabs(number)); // 函数作为实参

注意:函数可以作为函数的实参,如果要作为形参,必须使用函数指针。

在一个函数中调用另一个函数具备以下条件:

  • 被调用的函数必须是已经定义的函数。

  • 若使用库函数,应在本文件开头用 #include 包含其对应的头文件。

  • 若使用自定义函数,自定义函数又在主调函数的后面,则应在主调函数中对被调函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。

函数的声明

函数调用时,往往要遵循先定义,后使用,但如果我们对函数的调用操作出现在函数定义之前,则需要对函数进行声明。

定义

完整的函数使用分为三部分:

  • 函数声明

    c 复制代码
    int max(int x, int y, double z); // 函数声明只保留函数头,便于编译系统进行检查
    int max(int, int, double); // 函数声明的时候,可以省略形参名称

    函数声明如果是在同一个文件,一定要定义在文件中所有函数定义的最前面。如果有对应的 .h 文件,可以将函数的声明抽取到.h中。

  • 函数定义

    c 复制代码
    int max(int x, int y, double z) // 函数定义时,一定不能省略形参名称
    {
        return x > y ? x : y > z ? y : (int)z;
    }

    函数定义的时候,不能省略形参的数据类型、参数个数、参数名称,位置要和函数声明完全一致。

    注意:函数定义时参数列表要和函数声明时的参数列表完全对应,同时函数定义要保留形参名称

  • 函数调用

    c 复制代码
    int main()
    {
        printf("%d\n", max(4,5,6));
    }
作用

C语言的函数声明时为了提前告诉编译系统函数的名称、返回类型和参数,这样在函数实际定义之前就能安全调用它,避免编译错误,同时检查参数和返回值是否正确。相当于给编译器一个"预告",确保代码正确编译和运行。

使用
  • 错误示例:被调函数写在主调函数之后

    c 复制代码
    // 主调函数
    int main()
    {
        printf("%d\n", add(12,13));// 编译报错,因为函数未经过声明,编译系统无法检查函数的合法性
    }
    // 被调函数
    int add(int x, int y)
    {
        return x + y;
    }
  • 正确示例:主调函数写在被调函数之后

    c 复制代码
    // 被调函数
    int add(int x, int y)
    {
    return x + y;
    }
    // 主调函数
    int main()
    {
    printf("%d\n", add(12,13));
    }

    注意:如果函数的调用比较简单,如a函数调用b函数,b函数定义在a函数之前,此时是可以省略函数声明的。

  • 正确演示:被调函数和主调函数无法区分前后,必须要增加函数声明

    c 复制代码
    // 函数声明
    void funa(int, int);
    void funb(int, int);
    // 函数定义
    void funb(int a, int b)
    {
        ...
            // 函数调用
            funa();
    }
    void funa(int a, int b)
    {
        ...
            // 函数调用
            funb();
    }
    int main()
    {
        // 函数调用
        funa(12,13);
    }   

    声明的方式

    • 函数头加上分号

      c 复制代码
      int add(int a, int b);
    • 函数头加上分号,可省略形参名称,但不能省略参数类型

      c 复制代码
      int add(int, int);

变量和函数底层工作原理【扩展】

变量的底层执行机制

变量本质是内存中的一块存储空间,其底层处理涉及编译期的符号解析运行时的内存分配与访问

  1. 编译阶段:符号表与地址映射
  • 编译器在编译时会为每个变量创建符号表条目,记录变量名、类型、作用域和内存偏移量(而非实际地址)。

  • 对于全局变量和静态变量,编译器会将其分配到数据段 (已初始化)或BSS (未初始化),并计算其在段内的偏移量。

  • 对于局部变量,编译器会记录其在栈帧中的相对位置(基于栈指针的偏移量)。

  1. 运行阶段:内存分配与访问
  • 全局 / 静态变量:程序加载时,操作系统会将数据段和 BSS 段加载到内存的固定位置,变量的实际地址 = 段起始地址 + 编译期计算的偏移量。

  • 局部变量:函数调用时,CPU 会为函数创建栈帧,局部变量的地址 = 栈指针

(SP) + 编译期确定的偏移量(通常为负数,因为栈向下生长)。

  • 动态变量(malloc :通过系统调用在中分配内存,返回的指针是堆中实际地址,由内存管理模块(如 glibc 的 ptmalloc)维护。
  1. 访问变量的底层指令

访问变量时,CPU 通过地址计算 得到内存地址,再执行加载(load)或存储(store )指令。

例如, int a = 5; 会被编译为:计算 a 的地址,然后执行store 5 到该地址

函数的底层执行机制

函数的执行本质是指令流的跳转与栈帧管理,涉及函数调用、栈帧创建、参数传递和返回值处理。

  1. 编译阶段:函数地址与指令生成

编译器将函数体编译为一系列机器指令,存储在代码段(只读),并在符号表中记录函数名与起始地址。

函数参数和返回值的传递方式(如栈传递、寄存器传递)由调用约定(如 cdecl、stdcall)决定,编译器会按约定生成对应指令。

  1. 函数调用的底层步骤

步骤 1:参数入栈

调用者将参数按约定顺序(通常从右到左)压入栈中,或放入指定寄存器(如

x86-64 的部分参数用寄存器传递)。

步骤 2:保存返回地址

CPU 将下一条指令的地址(函数调用后的执行点)压入栈中,供函数返回时使

用。

步骤 3:跳转至函数入口

执行 call 指令,将程序计数器(PC)设置为函数的起始地址,开始执行函数指令。

步骤 4:创建栈帧

函数执行的第一条指令通常是:asm

c 复制代码
push ebp ; 保存调用者的栈帧基址
mov ebp, esp ; 用当前栈指针作为新栈帧的基址
sub esp, N ; 为局部变量分配N字节的栈空间

此时栈帧包含:参数、返回地址、上一个栈帧基址(ebp)、局部变量。

步骤 5:执行函数体

按编译生成的指令执行逻辑,访问局部变量(通过 ebp 偏移)、操作参数(通过ebp 正偏移)。

步骤 6:返回结果

返回值通常存入指定寄存器(如 x86 的 eax ,x86-64 的 rax ),或通过栈传递(大型结构体)。

步骤 7:恢复栈帧并返回

执行:asm

c 复制代码
mov esp, ebp ; 释放局部变量的栈空间
pop ebp ; 恢复调用者的栈帧基址
ret

关键底层概念

  • 内存分段:代码段(指令)、数据段(全局变量)、BSS 段(未初始化全局变量)、栈(局部变量 / 函数调用)、堆(动态内存)。

  • 栈帧:每个函数调用对应一个栈帧,包含参数、返回地址、局部变量,由 ebp(基址指针)和 esp(栈指针)界定。

  • 地址绑定:变量和函数的地址在编译期(静态绑定)或加载 / 运行期(动态绑定,如共享库)确定。

总结

  • 变量:通过编译期符号表记录偏移量,运行时映射到实际内存地址,通过 CPU 的加载/ 存储指令访问。

  • 函数:通过 call 指令跳转至代码段执行,借助栈帧管理参数、局部变量和返回地址,最终通过 ret 指令返回。

2:保存返回地址

CPU 将下一条指令的地址(函数调用后的执行点)压入栈中,供函数返回时使

用。

步骤 3:跳转至函数入口

执行 call 指令,将程序计数器(PC)设置为函数的起始地址,开始执行函数指令。

步骤 4:创建栈帧

函数执行的第一条指令通常是:asm

c 复制代码
push ebp ; 保存调用者的栈帧基址
mov ebp, esp ; 用当前栈指针作为新栈帧的基址
sub esp, N ; 为局部变量分配N字节的栈空间

此时栈帧包含:参数、返回地址、上一个栈帧基址(ebp)、局部变量。

步骤 5:执行函数体

按编译生成的指令执行逻辑,访问局部变量(通过 ebp 偏移)、操作参数(通过ebp 正偏移)。

步骤 6:返回结果

返回值通常存入指定寄存器(如 x86 的 eax ,x86-64 的 rax ),或通过栈传递(大型结构体)。

步骤 7:恢复栈帧并返回

执行:asm

c 复制代码
mov esp, ebp ; 释放局部变量的栈空间
pop ebp ; 恢复调用者的栈帧基址
ret

关键底层概念

  • 内存分段:代码段(指令)、数据段(全局变量)、BSS 段(未初始化全局变量)、栈(局部变量 / 函数调用)、堆(动态内存)。

  • 栈帧:每个函数调用对应一个栈帧,包含参数、返回地址、局部变量,由 ebp(基址指针)和 esp(栈指针)界定。

  • 地址绑定:变量和函数的地址在编译期(静态绑定)或加载 / 运行期(动态绑定,如共享库)确定。

总结

  • 变量:通过编译期符号表记录偏移量,运行时映射到实际内存地址,通过 CPU 的加载/ 存储指令访问。

  • 函数:通过 call 指令跳转至代码段执行,借助栈帧管理参数、局部变量和返回地址,最终通过 ret 指令返回。

相关推荐
剑神一笑12 分钟前
Linux pgrep 命令详解:按名称查找进程 PID 的高效方法
linux·运维·chrome
剑神一笑32 分钟前
Linux killall 命令详解:按进程名批量终止进程的原理与实践
linux·运维·chrome
isyangli_blog2 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008112 小时前
FastAPI APIRouter
开发语言·python
Benszen2 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆2 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木2 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充3 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~3 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6163 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang