函数间参数传递

官方信息技术老师·20 页·深入(追求细节与边界)·0 次浏览·2 天前
值传递地址传递返回值内存视角

函数间参数传递

看清值传递、地址传递与返回值的本质差异

按 空格/→ 演示下一步

1 / 20 页

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

值传递地址传递返回值内存视角

函数间参数传递

看清值传递、地址传递与返回值的本质差异

1第 1 页 · 函数间参数传递

什么是形参和实参

上页说到函数之间通过参数交换数据。但这'参数'二字其实有两种身份——一个是调用时实实在在传进去的,另一个是函数定义时只在纸上写的占位符。今天把它们拆开看。

实参
调用函数时括号里实际传入的数据,可以是变量、常量、表达式
形参
函数定义时声明的占位变量名,函数体内用它指代传入值
位置对应
实参按顺序一一对应到形参,名字可以不同但位置要匹配
局部生命周期
形参只在本次调用内存在,函数返回即销毁,作用域限于函数体
原件与复印件对应 →实参与形参

你手里拿着的原件是实参;函数内部用的形参是复印件,函数一返回就销毁

2第 2 页 · 什么是形参和实参

值传递的内存模型

从栈帧视角看值传递:调用方与被调方各持一份独立变量。

图解渲染中…
a2调用方栈帧中的原始变量b3被调方栈帧中的形参副本a4形参修改后实参不受影响
3第 3 页 · 值传递的内存模型

值传递代码演示

python

用一段完整代码演示整型与浮点型的值传递:函数内修改形参,外部实参纹丝不动。

代码高亮加载中…

值传递发生在调用瞬间:实参的值被复制一份给形参。函数内对形参的修改(a = a+10, b = b*2)只作用在这份副本上,因此外部 x、y 完全不受影响。

4第 4 页 · 值传递代码演示

形参改变会影响实参吗

验证:值传递的本质是单向复制

形参改变会影响实参吗
验证:值传递的本质是单向复制
5第 5 页 · 形参改变会影响实参吗

为什么数组传参特殊

上一页我们看到一个结论:值传递就是把数据完整复制一份给形参。传 4 字节的 int 时几乎没成本,但换成几 MB 甚至几 GB 的数组呢?

大数组复制代价高
几 MB 到几 GB 的数组,拷贝每个元素都要花时间和内存
开销是线性的
数组长度翻倍,复制时间和占用内存也翻倍,是 O(n) 复杂度
值传递不再划算
与其完整复制,不如让形参和实参直接看同一份数据
复印一本百科全书对应 →复制一个大数组

一千页全印一份又慢又贵,不如两人共看同一本

复制开销=O(n)×s(n=元素数, s=单元素大小)\text{复制开销} = O(n) \times s \quad (n=\text{元素数},\ s=\text{单元素大小})
6第 6 页 · 为什么数组传参特殊

数组名的本质

从左到右看:声明数组→分配内存→数组名a指向首地址→a是常量指针

图解渲染中…
a1如 int a[5] 这类声明b15个int共20字节连续存放d1数组名在多数表达式中退化为指针e1int*即指向int的指针类型
7第 7 页 · 数组名的本质

数组传参的内存布局

指针传递:形参接收地址而非副本

数组传参的内存布局
指针传递:形参接收地址而非副本
8第 8 页 · 数组传参的内存布局

一维数组传参代码

c

完整示例:打印数组、在函数里翻倍每个元素,看 main 里那个数组到底变没变。

代码高亮加载中…

L3 形参 int arr[] 实际等价于 int*;L17 真实数组在 main 栈帧;函数里 L12 一改,L26 看到的实参就跟着变——这正是数组传参区别于值传递的关键。

9第 9 页 · 一维数组传参代码

二维数组传参代码

c

二维数组传给函数时,列维度必须在编译期敲定,看这段代码怎么写才合法。

代码高亮加载中…

列维度是编译器计算 arr[i][j] 偏移地址的依据,行数则靠实参传入,因此形参中列必须写死、行可以省略。

10第 10 页 · 二维数组传参代码

数组传参 vs 指针传参

数组传参的 `[]` 外观容易让人误以为整组元素入栈,函数实际收到的是首元素地址。

数组传参
  • 形参可写成 `int a[]`
  • 实参可传 `&arr[0]`,`arr` 会转为地址
  • 函数内 `sizeof(a)` 只表示指针大小
指针传参
  • 形参写作 `int *p`
  • 实参可传 `&x`,或已有指针 `p`
  • 函数内 `sizeof(p)` 也只表示指针大小
正确的理解是:两者都传递首元素地址,因为数组形参会调整为指针;原数组长度不会随地址传入。
11第 11 页 · 数组传参 vs 指针传参

地址传递的副作用

想想 scanf("%d", &x):不传 &x 它就不知道把读到的数存哪去。这就是地址传递的典型场景——你不光把数据交给函数,还要允许它把结果"写回来"。

副作用现象
指针形参在函数内 *p=…,调用返回后实参已被改写
根因
形参和实参指向同一块内存,写一处两端都变
典型应用
swap 必须靠它;一个函数能一次返回多个结果
风险所在
无意修改、意外别名、跨线程竞争都源于共享
工程实践
只读指针加 const;C++ 优先用引用更安全
把钥匙借给朋友对应 →地址传递

他进屋挪家具,你回家布局已变——形参实参共享同一空间

12第 12 页 · 地址传递的副作用

return语句的作用

上一节我们看到,地址传递能让函数直接修改外部数据。但更多时候,函数只想把计算结果'告诉'调用者——这时它需要一个出口,把答案递回去。这个出口就是 return。

结束函数
执行到return立刻停,函数栈帧随后被回收
携带返回值
把一个值递回给调用者,作为这次调用的结果
void也能return
无返回值的函数可用return;提前退出,参数照常传入
自动售货机对应 →return语句

投币=传参,按选择键=调用,掉出商品=return返回值

13第 13 页 · return语句的作用

返回值的传递机制

从左到右看返回值的三条传递路径:基本类型走寄存器,大结构体走主调提前准备的栈内存。

图解渲染中…
b3整型/指针≤8字节:调用约定规定用rax带回b4浮点类型单独走XMM寄存器b5主调先在栈上预留空间,用隐藏指针传给被调
14第 14 页 · 返回值的传递机制

返回值使用示例

c

演示函数返回值的三种典型用法:赋值、嵌入表达式、直接传给其他函数。

代码高亮加载中…

L12 把返回值存进变量;L16 返回值直接参与算术运算;L20 返回值不落地,直接喂给 printf。返回值本质就是数据,能出现在任何需要值的位置。

15第 15 页 · 返回值使用示例

返回值的边界情况

返回值机制已经讲明白了,但有两个边界情况最容易翻车——返回局部变量指针的陷阱,和声明「无返回」的 void。

栈帧随函数销毁
return 时函数栈帧弹出,局部变量随内存被回收
返回局部变量指针
调用方拿到指针时数据还在,下次写入覆盖就出乱
void 返回类型
明确无返回值,编译器会阻止你把调用结果赋给变量
酒店退房后还拿房卡对应 →返回局部变量指针

退房后房卡上的房间号还在,但房内已被新住客重整,下次访问读到的是别人的东西

16第 16 页 · 返回值的边界情况

值传递 vs 地址传递 vs 返回值

传参有进有出:值传递、地址传递负责把数据送进函数,返回值负责把结果带出来。混了方向和机制,调用关系就乱。

入向:值/地址传递
  • 数据流向:调用方 → 函数内部
  • 值传递改不动实参,地址传递可间接修改
  • 适合喂数据,或需函数改多个外部变量
出向:返回值
  • 数据流向:函数内部 → 调用方
  • return 返回的是结果副本,调用方接收
  • 适合只产出一个结果,或返回状态码
要函数改外面多个变量 → 选地址传递;只要一个结果 → 用返回值;只是喂数据不需回传 → 值传递最干净。三者经常组合用。
17第 17 页 · 值传递 vs 地址传递 vs 返回值

自测:参数传递小测

点击作答

对函数 void f(int *p) { p = NULL; },调用 f(ptr) 后实参 ptr 的值会怎样?

18第 18 页 · 自测:参数传递小测

核心要点回顾

  • 三种传递通道:值、地址、返回值
  • 能否修改外部,取决于是否传入地址
  • 数组名即首地址,数组传参天然按地址走
  • 选哪种传递方式 = 划定函数的影响边界
延伸主题:const保护参数不被改坏函数指针与回调机制结构体应该按值还是按址传
19第 19 页 · 核心要点回顾

课后思考

先合上屏幕想一会儿,再翻参考答案。三个问题各对应一个层次。

1为什么值传递中形参的改变不会影响实参?请从内存模型的角度说明。

参考答案值传递会在栈上为形参分配独立空间,形参只是实参的副本。修改副本不会改动实参所在内存。这正是值传递的隔离特性。

2如果让你写一个交换两个变量值的函数,会选值传递还是地址传递?为什么?

参考答案必须用地址传递。值传递传的是副本,函数内交换的是副本本身,原变量不受影响。地址传递传指针,可直接改原内存。

3函数返回局部变量的地址,在函数外使用会发生什么?编译器会怎么处理?

参考答案返回的是栈上局部变量的地址,函数返回后栈帧销毁,该地址成为悬挂指针,访问是未定义行为。这是返回值的关键边界。

20第 20 页 · 课后思考