04. 函数和函数调用机制

40 阅读5分钟

1. 先学习/复习C语言的入门知识

1.1 C语言简介

C语言是一种通用的编程语言,于1972年由丹尼斯·里奇(Dennis Ritchie)创建。C语言最初目的是为了开发UNIX操作系统,但由于其简洁的语法、快速的执行速度和可移植性,自此成为了一种广泛应用的系统和应用程序开发语言。

1.2 基本数据类型

C语言中的基本数据类型有整数(int)、浮点数(float、double)、字符(char)和布尔(bool)。下面是一些常用的基本数据类型示例:

int num = 10;  // 整数类型变量
float pi = 3.14159;  // 单精度浮点数类型变量
double money = 1000.5;  // 双精度浮点数类型变量
char letter = 'A';  // 字符类型变量
bool isTrue = true;  // 布尔类型变量

1.3 变量

在C语言中,需要使用变量来存储和操作数据。变量在使用前需要先声明,声明变量时需要指定变量的类型。例如:

int a;  // 定义整数类型变量a
float b;  // 定义单精度浮点数类型变量b
double c;  // 定义双精度浮点数类型变量c
char d;  // 定义字符类型变量d
bool e;  // 定义布尔类型变量e

在C语言中,变量需要初始化后才能使用。可以在声明变量时进行初始化:

int a = 10;  // 定义整数类型变量a并初始化为10
float b = 3.14;  // 定义单精度浮点数类型变量b并初始化为3.14
char c = 'A';  // 定义字符类型变量c并初始化为'A'
bool d = true;  // 定义布尔类型变量d并初始化为true

1.4 运算符

C语言中常用的运算符包括算术运算符、关系运算符、逻辑运算符和三目运算符等。

算术运算符:

int a = 10, b = 20;  // 定义变量a和b

int sum = a + b;  // 相加
int diff = a - b;  // 相减
int mul = a * b;  // 相乘
int div = b / a;  // 相除
int mod = b % a;  // 取模

关系运算符:

int a = 10, b = 20;  // 定义变量a和b

bool isEqual = a == b;  // 是否相等
bool isLess = a < b;  // 是否小于
bool isGreater = a > b;  // 是否大于

逻辑运算符:

bool isTrue = true, isFalse = false;  // 定义变量isTrue和isFalse

bool andResult = isTrue && isFalse;  // 逻辑与
bool orResult = isTrue || isFalse;  // 逻辑或
bool notResult = !isTrue;  // 逻辑非

三目运算符:

int a = 10, b = 20;  // 定义变量a和b

int max = a > b ? a : b;  // 如果a>b,max等于a,否则等于b

1.5 控制流

C语言中常用的控制流语句包括if语句、for循环、while循环和switch语句等。

if语句:

int a = 10, b = 20;  // 定义变量a和b

if(a > b) {  // 如果a>b,执行以下语句
    printf("a大于b\n");
} else {  // 否则执行以下语句
    printf("a小于等于b\n");
}

for循环:

int i;  // 声明计数器变量i

for(i = 0; i < 10; i++) {  // 循环10次
    printf("%d\n", i);  // 打印当前计数器的值
}

while循环:

int i = 0;  // 声明计数器变量i

while(i < 10) {  // 循环10次
    printf("%d\n", i);  // 打印当前计数器的值

    i++;  // 计数器每次循环自增1
}

switch语句:

int a = 1;  // 定义变量a

switch(a) {
    case 0:  // 如果a等于0,执行以下语句
        printf("a等于0\n");
        break;
    case 1:  // 如果a等于1,执行以下语句
        printf("a等于1\n");
        break;
    default:  // 如果a不等于0或1,执行以下语句
        printf("其他情况\n");
        break;
}

1.6 函数

C语言中函数是一种独立的代码模块,用于完成特定的任务。函数包含函数头和函数体两部分。

函数头包括函数名、返回值类型和参数列表等。例如:

int add(int a, int b) {  // 定义add函数,包含两个整数类型的参数a和b,返回值为整数类型
    int sum = a + b;  // 计算a和b的和
    return sum;  // 返回计算结果
}

调用add函数:

int result = add(10, 20);  // 调用add函数,将10和20传入
printf("10和20的和为:%d\n", result);  // 打印结果

上面的程序会打印出以下内容:

10和20的和为:30

2. 理解函数

在初高中阶段,函数通常是指数学函数的概念。

数学函数是一种关系,它将一些输入值(称为自变量)映射到相应的输出值(称为因变量)。一般这样表示:y = f(x)。

初高中学习的函数和编程中的函数是有一些相似之处的。在编程中,函数用于封装可被重复使用的代码块,接收输入和返回输出,这和初中学习的函数有些相似之处。

C 语言中的函数是一种可以被重复执行并且可以接收输入和返回输出的代码块。函数可以减少代码的重复性,提高程序的可读性和可维护性。

C 语言的函数的格式包括函数名、参数列表、函数体以及返回值。

int add(int x,int y){
    int a = x + y;
    return a;
}
  • 函数名:add
  • 参数列表:xy,它们的类型都是int
  • 函数体:{}大括号里面的内容都是函数体
  • 返回值:return后面的值是返回值,因为add函数前面的int指定了返回值的类型,所以这里返回int类型的a

函数就是输入一些值,进行一些操作或处理,然后输出一些值!

3. 什么是栈?

把书或盘子摞起来,就像压栈一样;需要一次一个地取走它们,就像弹栈一样。

栈是一种常见的线性数据结构,它遵循后进先出(LIFO)的原则。栈可以看作是一个容器,其中的元素按照后进先出的顺序进行存储和访问。

3.1 后进先出

后进先出(Last In First Out,LIFO)的特性。当数据被插入栈中时,它会被放置在栈顶。而当从栈中弹出数据时,最后插入栈的数据会首先被弹出。

  • 首先,我们向栈中插入元素 A。由于栈是空的,插入的元素就变成了栈顶。

  • 然后,我们又向栈中插入元素 B。由于栈的特性是后进先出,B 就被放在了 A 的上面,即成为了栈顶元素。

  • 接下来,我们再插入一个元素 C,同样的,它会被放在栈顶。

  • 当我们从栈中弹出元素时,我们会弹出栈顶元素。因此,我们先弹出的是 C,随后是 B,最后是 A。

3.2 入栈和出栈

入栈 和 出栈 是栈的两个基本操作。

  1. 入栈(Push)操作: 入栈是将一个新的元素添加到栈的顶部,也就是在栈中创建一个新的节点。新元素被放置在栈顶之上,并成为新的栈顶元素。

  2. 出栈(Pop)操作: 出栈是从栈顶移除一个元素,即删除栈顶节点并返回其值。

3.3 类似于栈结构的场景

  • 浏览器的后退和前进:每次访问新页面时,该页面的信息都会被压入栈中,如果用户单击“后退”按钮,则最近访问的页面会从栈中弹出,以便用户返回上一个页面
  • 撤销操作:先恢复的最新的状态,直到回到所需的状态为止。
  • APP的点击:先点击进入商品列表页面,然后进入商品详情页面,再进入购买页面,先返回详情页面,再返回商品列表页面。

4. 栈内存和函数调用

4.1 栈内存与栈帧

栈内存(Stack Memory)是指程序在运行过程中用于存放栈的一段内存空间。栈内存是计算机内存中的一部分,通常大小是固定的,在程序的运行过程中,栈内存会被分成多个栈帧(Stack Frame)来存放程序需要临时保存的数据,比如函数的参数、局部变量、返回地址等等。

每当一个函数或过程被调用时,都会创建一个对应的栈帧,栈帧会在调用结束后被销毁。

栈帧主要包含以下信息:

  1. 返回地址(Return Address):记录了当前函数或过程执行完后,程序需要返回到哪个位置继续执行。

  2. 参数和局部变量(Parameters and Local Variables):用于存储函数或过程的输入参数、局部变量的值以及临时变量的空间。

  3. 调用者保存的寄存器(Caller-Saved Registers):保存了在函数调用之前由调用者保存的寄存器的值。

  4. 栈指针(Stack Pointer):记录了当前栈帧的边界,即栈顶位置。

4.2 函数调用为什么用到栈?

假设A函数调用B函数,B函数调用C函数,那么函数调用时栈的运作方式如下图所示:

当A函数调用B函数时,A的状态信息(如返回地址、参数等)被压入栈中,同时分配一块新的栈帧给B函数来保存B的状态信息。当B函数再调用C函数时,B的状态信息也会被压入栈中,C函数同样会被分配一块新的栈帧来保存C的状态信息。

当C函数执行完毕后,控制权回到B函数,并弹出C的栈帧,恢复到B函数的执行状态。同样,当B函数执行完毕后,控制权回到A函数,并弹出B的栈帧,恢复到A函数的执行状态。

函数调用使用栈的主要原因是为了实现函数调用的嵌套和返回的正确性。

当一个函数被调用时,它的参数、局部变量以及一些其他信息需要被保存起来,以便在函数执行完毕后能够被恢复。这些信息通常保存在函数的栈帧中。同时,调用者也需要存储一些信息,例如调用该函数前的执行状态,以便在函数返回后能够正确恢复。因此,调用者的信息也需要保存在栈中。

栈作为一种后进先出(LIFO,Last-In-First-Out)的数据结构,非常适合用于保存函数调用相关的信息。每当调用一个函数时,会创建一个新的栈帧,并将其压入栈中,即将栈指针下移。而当函数返回时,栈帧就会被弹出,即将栈指针上移,使得上一个栈帧成为当前栈帧。

5. 函数调用机制

一个简单的C语言代码:main函数调用A函数,A函数调用B函数,B函数调用C函数。

#include <stdio.h>

int C(int z){
	return z;
}

int B(int y){
	int res = C(3);
	return y + res;
}

int A(int x){
	int res = B(2);
	return x + res;
}

int main(){
   int a = A(1);
   printf("%d",a);
   return 0;
}
// 打印结果
// 6

  • ①main函数调用A函数,传入参数1
  • ②A函数调用B函数,传入参数2
  • ③B函数调用C函数,传入参数3
  • ④C函数将数值3返回给B函数
  • ⑤B函数将2和3相加的结果5返回给A函数
  • ①A函数将1和5相加的结果6返回给main函数

在程序运行的时候会创建一个栈内存区域,由于每个函数都有自己的栈帧,所以我们可以分别观察每个函数在调用时栈内存的变化。

ebp寄存器在函数调用和返回过程中起到重要的作用,主要有以下几个方面的功能:

  1. 函数调用时,ebp用于保存调用者函数的栈帧指针。当一个函数被调用时,它会在栈上分配一个新的栈帧来保存该函数的局部变量、参数和其他必要的信息。将调用者函数的ebp保存在当前函数的栈帧中,可以在当前函数执行完毕后恢复调用者函数的上下文。

  2. 在函数执行期间,ebp用于定位栈上的局部变量和函数参数。通过ebp,函数可以通过相对于EBP的偏移来访问其参数和局部变量,而不受栈帧的变化影响。这样可以在函数中方便地引用和修改局部变量和参数。

  3. 函数返回时,ebp用于恢复调用者函数的栈帧指针和上下文。当一个函数执行完毕后,它会从栈上弹出自己的栈帧,并恢复调用者函数的栈帧指针(即调用者函数的ebp)。这样就可以在返回后,继续执行调用者函数的代码,而不会导致栈帧混乱。

6. 总结

本文首先介绍了C语言的基本概念,包括数据类型、变量、表达式、流程控制语句等,从而为后续学习打下基础。在此基础上,我们阐述了函数调用机制,深入探讨了栈、栈内存和栈帧等概念,从而使读者能够全面了解函数调用的过程和栈帧的作用。

关注微信公众号:“小虎哥的技术博客”,让我们一起成为更优秀的程序员❤️!

文章和代码仓库:

gitee(推荐):gitee.com/cunzaizhe/x…

github:github.com/tigerleeli/…