函数 栈帧

函数

引用式声明(函数原型):简称声明,指定了函数的返回值类型、函数的接受参数类型,这些叫函数的签名。与变量相同,在函数声明时编译器不会为函数分配内存。函数的返回值类型在C99之前若不指明则默认隐式转换为为int类型,但在C99之后必须指明函数的返回值类型。

定义式声明:简称定义,提供了函数的具体实现。不同于变量的定义,函数在定义时不会被分配内存,在函数调用时候编译器在栈帧上为函数分配内存。相同函数可以有多个声明,但只能有一个定义。

形参表:函数在声明或定义时的参数,注意形参只是声明而非定义。形参表在函数声明时可省略形参标识符(形参名),但不能省略形参的数据类型

实参表:函数在调用时实际传入的值

函数调用:主调函数(主动调用其他函数的函数) 被调函数(被其他函数调用的函数) 函数的嵌套调用 函数的递归调用

形参只是声明而非定义

形参在函数调用时会被赋予实际参数的值,而不是独立存在的变量。

  1. 形参的性质:形参是函数定义中的参数,在函数被调用时用于接收传递给函数的实际参数的值。形参在函数定义时就已经确定了,它们的作用是接收参数值,而不是独立的变量。因此,形参在函数定义中只是参数的声明,用于说明函数接收的参数类型和名称,而不是独立的变量定义。

  2. 形参和实参的关系:在函数调用时,实际参数的值会被传递给形参,形成了形参和实参的对应关系。形参会在函数执行期间使用实参的值,但形参本身并不是独立的变量,它们不会在函数调用之前分配内存空间或提供初始值。

  3. 形参在函数调用时赋值:当函数被调用时,实际参数的值会被赋给形参,形成了形参和实参之间的对应关系。形参在函数执行期间会被视为局部变量,但它们的值是由函数调用时的实际参数决定的,而不是在函数定义时确定的。

因此,形参在函数定义中只是参数的声明,用于说明函数接收的参数类型和名称,而不是独立的变量定义。形参的特殊性质使得它们只能被视为声明而不是定义。

栈帧

栈帧,也称为活动记录、调用帧或过程帧,是在函数调用时在函数调用栈中分配的一块内存区域,用于存储函数的局部变量、参数值、函数返回地址以及其他与函数调用相关的信息。

每当函数被调用时,都会在栈上创建一个新的栈帧,栈帧在函数调用结束后会被销毁。栈帧的创建和销毁都是由编译器和执行环境来管理的。

典型的栈帧结构包括以下几个部分:

  1. 局部变量区:用于存储函数内部声明的局部变量,这些变量在函数调用时被创建,在函数返回时被销毁。

  2. 参数区:用于存储函数调用时传递的参数值。

  3. 返回地址:指向调用该函数的指令地址,函数执行完毕后会返回到该地址继续执行。

  4. 旧的基址指针:指向调用该函数的上一个栈帧的基址指针,用于在函数返回时恢复上一个栈帧。

  5. 其他辅助信息:如编译器生成的临时变量、寄存器保存等。

栈帧的存在使得函数调用能够有效地管理函数的局部数据和调用关系。每个函数调用都会在栈上创建一个新的栈帧,形成一条栈链,函数的局部变量和参数在各自的栈帧中进行存储,保证了函数调用之间的隔离性。

从栈帧角度看函数调用

基址指针,是在函数调用时用于帮助在栈上定位局部变量和参数的指针。基址指针指向当前函数栈帧的基址,也就是栈帧中局部变量和参数的起始地址。

在一些体系结构中,特别是x86架构下的C语言编程中,通常使用基址指针和栈指针来进行栈操作。栈指针指向当前栈顶,而基址指针则通常指向当前栈帧的底部。

基址指针的主要作用是:

  1. 定位局部变量和参数:通过基址指针加上偏移量来访问函数的局部变量和参数。
  2. 帮助在函数调用中保存和恢复现场:基址指针在函数调用时被保存在当前函数栈帧中,在函数返回时用于恢复上一个函数栈帧的状态,以便程序能够正确地返回到调用函数的位置。

在函数调用过程中,基址指针和栈指针的变化和协作是确保函数调用能够正确执行的关键。

当一个函数被调用时,会发生以下步骤:

  1. 保存上一个栈帧的状态:调用函数时,当前函数的栈帧会被压入调用栈,此时需要保存上一个栈帧的状态。这包括保存上一个栈帧的基址指针(旧的基址指针)和返回地址。这些信息保存在当前栈帧中以便在函数返回时能够恢复到上一个栈帧的状态。

  2. 分配当前函数的栈帧:为当前函数调用分配一个新的栈帧,包括存储局部变量、参数和其他与函数调用相关的信息。

  3. 传递参数:将调用函数时传递的参数值复制到当前函数的栈帧中的参数区域。

  4. 执行函数体:开始执行被调用函数的代码,包括对局部变量的操作和执行其他语句。

  5. 返回地址更新:当遇到函数返回语句时,执行流程会返回到调用该函数的指令处。这时会使用保存在当前栈帧中的返回地址,跳转到调用处继续执行。

  6. 恢复上一个栈帧状态:在函数返回时,需要恢复上一个栈帧的状态,包括恢复旧的基址指针和返回地址,以便继续执行上一个函数的代码。

  7. 释放当前栈帧:当前函数执行完毕后,它的栈帧会被销毁,释放栈上的内存空间。

这样,函数的调用就完成了。整个过程中,栈帧的切换和状态保存使得程序能够正确地管理函数调用关系,并保证函数调用的正确执行。

相关推荐
奋斗的小花生10 分钟前
c++ 多态性
开发语言·c++
魔道不误砍柴功12 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨15 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程42 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
萧鼎3 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸3 小时前
【一些关于Python的信息和帮助】
开发语言·python