函数

官方信息技术老师·12 页·深入(追求细节与边界)·0 次浏览·3 天前
调用机制作用域闭包高阶函数递归边界

函数

看清函数的执行机制:作用域链、闭包捕获与递归终止条件

按 空格/→ 演示下一步

1 / 12 页

全部页面点击任意一页,跳回舞台从这页播放

调用机制作用域闭包高阶函数递归边界

函数

看清函数的执行机制:作用域链、闭包捕获与递归终止条件

1第 1 页 · 函数

函数初识

上页留下的'函数'还是直觉印象,本页要收紧边界:什么关系算函数、什么只是相关?靠的是'每个输入只对应唯一输出'这条铁律。

严格定义
每个输入 x 都有且仅有唯一输出 y 与之对应
定义域与值域
输入的允许范围叫定义域,输出的到达范围叫值域
三种表示
解析式、图像、表格三者等价,各有适用场景
本质是映射
函数是从集合 A 到集合 B 的一种对应规则
自动售货机对应 →函数

投币(输入)必得唯一商品(输出),机器就是对应规则

f:AB, xA, !yB, y=f(x)f: A \to B,\ \forall x \in A,\ \exists!\, y \in B,\ y = f(x)
2第 2 页 · 函数初识

函数的参数传递

上一节我们让函数跑起来了,但你有没有想过:调用 f(x) 时,那个 x 究竟是怎么进到函数里的?是把你传的东西复印了一份,还是只抄了个门牌号?

形参 vs 实参
形参是定义时的占位符,实参是调用时真正传入的值
传的是引用
Python 里参数名只是给实参对象贴了一张新标签,并非拷贝
可变性决定副作用
可变对象能在函数内被原地修改,外部也会看到变化
默认参数的坑
def f(x=[]) 的默认值只在定义时创建一次,多次调用共享同一个列表
给物品贴标签对应 →参数传递

形参是新标签,实参是已存在物品;改物品内容两边都看见,重贴标签只动标签本身

3第 3 页 · 函数的参数传递

函数嵌套调用

上一节我们搞清楚了参数是怎么传给函数的。但如果函数 A 自己处理不了全部事情,需要临时叫 B 来帮忙,结果再拿回来接着用——这就是嵌套调用。

嵌套调用
函数A执行过程中再次调用函数B,B执行完毕后A继续运行
调用栈挂起与恢复
B被调用时A挂起并压栈,B返回时弹栈,A从断点继续
返回值接力
B的return值可直接作为表达式参与A后续运算
作用域彼此独立
A的局部变量B不可见,反之亦然——边界不会因嵌套而打开
主管请示总监对应 →外层调用内层函数

主管(A)处理到一半请总监(B)审批,总监签完(A继续);主管的项目(A局部变量)总监看不见

4第 4 页 · 函数嵌套调用

函数递归调用

上一页函数里可以调用别的函数。如果一个函数调用的正是它自己,事情会变得既优雅又危险——这就是递归。

递归定义
函数直接或间接调用自身
终止条件
至少一个 base case,否则无限调用
递归方程
把问题规模缩小,递推到终止条件
栈式执行
逐层压栈,到终止条件后回溯求值
俄罗斯套娃对应 →递归调用与终止

打开一层套一层,最里那个最小娃娃就是终止条件

f(n)={1n=1nf(n1)n>1f(n)=\begin{cases}1 & n=1\\ n\cdot f(n-1) & n>1\end{cases}
5第 5 页 · 函数递归调用

全局变量和局部变量

递归调用讲过,函数会一层层嵌套执行。问题来了:不同函数里定义的变量,到底谁能看见谁?这就涉及变量的「地盘」——作用域。

全局变量
定义在函数外部,整个文件里的函数都能读取
局部变量
定义在函数内部,只能在该函数体内访问
作用域与生命周期
全局随程序启动一直存活;局部随函数调用创建、返回时销毁
名字遮蔽
局部与全局同名时,函数内优先读局部,全局被「挡住」
global 声明
Python 中函数内修改全局变量需用 global 声明,否则只是新建局部变量
公司公告栏 vs 工位抽屉对应 →全局变量 vs 局部变量

公告栏谁都能看(全局),抽屉只有自己能开(局部);撞名时抽屉里的优先

6第 6 页 · 全局变量和局部变量

变量的存储类型

上一节我们区分了全局变量和局部变量,知道了作用域的不同。但变量还有另一个维度——「能活多久」,也就是它的存储类型。

auto 自动存储
局部变量默认类型,由编译器自动分配和释放栈内存
static 静态存储
使局部变量生命周期延长至程序结束,值跨调用保留
register 寄存器存储
建议编译器将变量放入 CPU 寄存器以加速访问
extern 外部存储
声明引用其它源文件定义的全局变量,实现跨文件共享
不同住宿身份对应 →变量的存储类型

auto 临时住退房即走,static 长住到期才走,register VIP 优先,extern 别处的房产证

7第 7 页 · 变量的存储类型

二级C考点解析之函数的定

上一节我们见过函数被调用——点菜让后厨做菜。但后厨这道'菜'是怎么写进菜单、又是怎么一步步做出来的?这就要拆解函数'定义'本身。

定义语法骨架
返回类型、函数名、形参列表、函数体四部分缺一不可
定义≠声明
定义带函数体;声明仅给原型,告诉编译器'有这么个函数'
返回类型必显式
void、int 等必须写出,缺省 int 是老标准遗留(考试慎选)
形参逐个声明类型
每个参数都写类型,如 int a, int b,不能简写为 int a, b
盖房子对应 →函数定义

声明是贴出'此处将建楼'的告示,定义是真正动工砌出整栋楼

返回类型 函数名(形参列表) { 函数体 }\text{返回类型}\ \text{函数名}(\text{形参列表})\ \{\ \text{函数体}\ \}
8第 8 页 · 二级C考点解析之函数的定

二级C考点解析之函数的嵌

函数的定义考点刚刚梳理完,现在轮到嵌套与递归。考场上这两类题看似简单,但命题人最爱在「定义 vs 调用」「递归终止条件」这些位置挖坑。

嵌套定义 vs 调用
C 不允许函数体内再定义函数,只能调用其他函数
递归三要素
终止条件、递推关系式、收敛方向,缺一不可
递归的栈机制
每次调用压一帧,return 时弹一帧,深度过大会栈溢出
典型考题模型
阶乘、Fibonacci、汉诺塔、字符串逆序,看执行结果为主
拆套娃对应 →递归调用栈

拆一层相当于一次递归调用入栈,套回去相当于 return 出栈

f(n)={1,n=1nf(n1),n2f(n) = \begin{cases} 1, & n=1 \\ n \cdot f(n-1), & n \geq 2 \end{cases}
9第 9 页 · 二级C考点解析之函数的嵌

二级C考点解析之变量的完

前页我们学了 auto、static、register、extern 四种存储类型的作用。但把它们和数据类型、变量名组合起来,一行完整的变量定义到底由哪几部分组成?哪些细节是考试中最容易丢分的盲区?

完整语法形式
[存储类型][数据类型] 变量名[=初值]; 四要素按此顺序排列
存储类型可省
省略时全局变量默认extern,局部变量默认auto
类型决定空间
int 4字节、char 1字节、double 8字节,属常考点
定义≠声明
声明(extern)只说明不分配空间,定义才真正分配内存
初值与默认
static未初始化默认0,auto未初始化值随机不确定
租房签约对应 →变量定义

租期类型=存储类型,户型=数据类型,门牌号=变量名,首月押金=初值

[存储类型]  [数据类型]  变量名[=初值];\text{[存储类型]}\;\text{[数据类型]}\;\text{变量名}\,[=\text{初值}];
10第 10 页 · 二级C考点解析之变量的完

本节要点

  • C 的参数传递本质是值传递;传地址是模拟引用效果的唯一手段
  • 递归需要递推式与终止条件同时成立,缺一就会栈溢出
  • 存储类别决定生命周期,与作用域是两套独立维度
  • 同名局部变量在不同函数中互不相干,各占独立栈帧
  • 函数嵌套调用靠栈帧层层叠加,深层递归易引发栈溢出
延伸主题:指针与函数:函数指针与回调机制递归转迭代:避免栈溢出的实战改写内存四区:栈区、堆区、全局区的边界
11第 11 页 · 本节要点

课后思考

先合上书本独立思考,再对照参考答案检验思路。三道题分别对应回顾、应用与拓展。

1递归调用中,每一层递归调用都会为参数和局部变量重新分配内存吗?为什么?

参考答案会。每次递归调用都会在栈上开辟新的栈帧,参数和局部变量是该栈帧的私有副本。递归返回时栈帧销毁,变量随之释放。

2把递归改写为循环时,递归中传递的'状态'通常要怎么处理?能否不依赖全局变量?

参考答案可以把状态作为函数参数显式传入(累加器模式),或封装到结构体中传入指针。这样既保留了局部性,又避免了全局变量的副作用。

3全局变量加 static 和不加 static,在多文件项目中有什么本质区别?

参考答案加 static 的是内部链接,只在当前源文件可见;不加的是外部链接,可被其他文件用 extern 引用。考试中常考察此区别。

12第 12 页 · 课后思考
函数 · 知识图解