函数定义

官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·2 天前
函数定义内存布局调用栈作用域

函数定义

看完你能讲清函数定义、参数传递机制与返回值的完整栈帧变化

按 空格/→ 演示下一步

1 / 17 页

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

函数定义内存布局调用栈作用域

函数定义

看完你能讲清函数定义、参数传递机制与返回值的完整栈帧变化

1第 1 页 · 函数定义

函数到底是什么

定义告诉你函数「长什么样」:名字、参数、返回值。但它真正在做什么?为什么说万物皆可函数化?走进一家工厂,从流水线里看清函数的本质。

输入-输出映射
原料进去、产品出来,中间过程被封装
确定性
同样的输入,必然产出同样的输出
副作用
返回值之外,悄悄改了文件、屏幕或全局状态
可组合
把多个函数串成更强大的处理流程
工厂对应 →函数

原料对应输入,流水线对应函数体,产品对应返回值

2第 2 页 · 函数到底是什么

函数的组成结构

从中心向外看, 四条分支对应一个函数语法上的四个槽位。

图解渲染中…
R语言允许时可写 void, 表示无返回值P无参数时可留空, 也可显式写 voidB花括号内是函数真正执行的代码块
3第 3 页 · 函数的组成结构

函数定义的基本语法

python

一段带类型注解的 Python 函数,能清晰看到返回类型、函数名、参数列表和函数体四部分。

代码高亮加载中…

L3 是函数名与 def 关键字,L4 是参数列表,L5 用 `->` 声明返回类型,L8 是真正返回值的语句;L11 演示如何调用并接收返回值。

4第 4 页 · 函数定义的基本语法

最简函数:无参无返回值

c

一个最小的 void 函数:不接收参数,也不返回结果,只负责执行一段动作。

代码高亮加载中…

L3 用 void 表示无返回值,() 表示无参数;L4 函数体内没有 return;L8 调用时直接写函数名加空括号。三处特征共同定义了一个『无参无返回值』函数。

5第 5 页 · 最简函数:无参无返回值

带参数函数的定义

python

通过 power 函数,看清形参声明、类型注解和不同调用方式。

代码高亮加载中…

形参在 def 行带类型注解;默认参数让调用更灵活;位置与关键字是实参传参的两种方式。

6第 6 页 · 带参数函数的定义

有返回值函数的定义

python

展示一个返回计算结果的函数,重点看 return 语句与返回类型如何一致。

代码高亮加载中…

L1 用 -> float 声明返回类型;L4 return 把值送出;L6 接住返回值;L7 用 type() 校验——三者共同保证「返回类型一致性」。

7第 7 页 · 有返回值函数的定义

形参和实参的本质区别

上一页我们写了带参数的函数,知道参数要写在 def 后面的括号里。但同一个括号里其实藏着两种完全不同的角色:定义时写的「形」,和调用时塞进去的「实」。这一页我们就拆清楚。

形参
函数定义时声明的名字,本质是局部变量;只在函数被调用时才存在
实参
调用时传入的具体值,可以是字面量、变量或任意表达式结果
绑定时机
调用瞬间实参的值被赋给形参,函数体里通过形参名操作这份数据
快递单上的收件地址对应 →形参与实参

地址栏是占位的形参,实际填写的「北京市海淀区中关村」是实参

8第 8 页 · 形参和实参的本质区别

值传递的完整过程

自上而下读,看实参 x 与形参 n 在调用前后的内存地址和值如何变化。

图解渲染中…
a1调用前,实参 x 位于调用方栈帧,地址 0x100,值为 10a2函数被调用,在新栈帧中分配形参 n,地址 0x200,初始未定义a3按值复制:把 x 的位模式原样写入 n,地址独立、值相同a4修改 n 不影响 x,这就是值传递的隔离性根源
9第 9 页 · 值传递的完整过程

传值 vs 传地址

传值和传地址表面上都是「传参数」,但本质区别是函数能否改动外部变量。混淆时会出现「代码跑了,原数据却没变」的诡异 bug。

传值(值传递)
  • 传递的是实参的副本
  • 函数内修改不影响原变量
  • 适合只读、计算等不改原数据的场景
传地址(址传递)
  • 传递的是实参的内存地址
  • 函数内修改会直接影响原变量
  • 适合需要修改外部数据的场景
当函数只读或计算时选传值(安全隔离);当需要函数修改外部变量时选传地址。判断标准就一条:函数是否要动形参之外的实参本身。
10第 10 页 · 传值 vs 传地址

参数传递的常见错误

c

数组名作为实参传入函数时,会自动退化为指针,长度信息丢失。

代码高亮加载中…

形参写 int arr[] 只是语法糖,编译器实际当成指针处理;函数内 sizeof(arr) 是 8 字节而非 20,改 arr[i] 等于直接改原数组。

11第 11 页 · 参数传递的常见错误

函数声明的语法

c

看原型声明里的形参名,为何不会与文件作用域的同名变量打架。

代码高亮加载中…

原型里小括号中的形参名只活在「函数原型作用域」里,声明结束就消散,不会污染外层同名变量;定义时形参名也可与原型不同,类型才是契约。

12第 12 页 · 函数声明的语法

声明与定义的区别与联系

一图看清两者的边界

声明与定义的区别与联系
一图看清两者的边界
13第 13 页 · 声明与定义的区别与联系

例题:求最大值函数

c

从需求分析到函数设计,再到完整实现,看一个求最大值函数如何诞生。

代码高亮加载中…

L11 是函数签名;L13 是带返回值的出口;L21 是实参 x、y 传给形参的过程。

14第 14 页 · 例题:求最大值函数

例题:阶乘递归函数

python

用阶乘演示递归:函数在自己的定义里调用自己,靠基准情形终止。

代码高亮加载中…

递归三要素齐了:函数定义(L1)、基准情形终止(L3)、递归调用自身(L6);L9 展示一次完整调用并打印。

15第 15 页 · 例题:阶乘递归函数

函数定义要点总结

  • 返回类型是函数的输出契约,无返回值也是一种约定
  • 参数传递本质都是值拷贝,区别只在于副本指向什么
  • 声明是接口、定义是实现,分离后编译器才能跨文件协作
  • 形参随每次调用诞生又消亡,递归就靠这个新生机制
  • 多数 bug 出现在边界:缺返回值、空指针、误改实参
延伸主题:作用域与生命周期函数指针与回调内联函数与宏的取舍
16第 16 页 · 函数定义要点总结

课后思考

先自己想,再对照参考答案——三个问题层层递进,答案点到为止。

1函数声明为什么要写在调用之前?编译器看到没声明的函数调用时,背后到底发生了什么?

参考答案C 语言要求先声明后使用——编译器从上往下扫描,看到调用时必须已经知道函数签名(返回类型、参数列表),否则无法做类型检查。声明就是给编译器的一张「身份证」。

2实际开发中,什么时候用值传递,什么时候传地址?只看「要不要改原值」够不够?

参考答案判断要看两方面:是否需要修改原值(语义),以及拷贝代价(性能)。结构体、大数组传地址更高效,普通 int、char 值传递更简单安全。

3递归求阶乘很优雅,但 n=10000 时程序会怎样?为什么?能用其他方式改写吗?

参考答案每次递归都会压栈,系统栈空间有限(通常 1~8MB),n 太大就会 stack overflow。改用循环迭代即可避免,且常数更小、效率更高。

17第 17 页 · 课后思考
函数定义 · 知识图解