递归调用与指针再探
递归调用与指针再探
背景:
依旧是辗转相除法求最大公约数
代码实现:
1 | |
函数 Gcd 在定义中调用了自身( return Gcd(b, a % b) ),这就是递归调用的特征——函数内部包含对自身的调用。
题目:
牛顿迭代法。用牛顿迭代法求,迭代公式为,x0=a.要求迭代的精度满足。如果迭代20次之后仍未能达到精度要求,也停止计算。
输入格式要求:”%f” 提示信息:”Input a=? “
输出格式要求:”\na=%.6f,x=%.6f,i=%d”
example:
Input a=?
9
a=9.000000,x=3.000000,i=6
代码实现:
1 | |
这个代码是我自己写的,感觉就是在没逼硬装
我想用新学的递归调用来做这个题的,但是发现递归不是单纯的等于for或者while循环,编程时还遇到了若干函数调用和指针的问题,导致这个程序我大概花了一个小时。总归是写出来了,让我们把这个代码研究清楚吧
我自己写markdown已经一个多月了,也做了很多题目。
在这个过程中,通过不断调试代码和询问AI,学到了很多关于这门语言的知识和相关注意事项。
一有所得我就写下来,并发表在github的个人blog上,我不知道这样学习代码的方法对不对,就我个人而言,我感觉我写代码没天赋,只能努力地去解决一个个问题再记下来
【1】减少冗余的参数传递
在Fx函数的定义中
起初我是这么写的:
1 | |
提示a没有被定义,我才发现还需要把a的初始值传递到封装函数中,于是
修改成最终模样:
1 | |
会发现在Fx函数的定义中,常量a的值是a,float a 的初始值也是a,那么可不可以简化一下,只用传递一个a,将作为x的初始值、计算式中的常量?
可以,修改一下:
1 | |
只传递一个a值,并在Fx函数中第一时间把a赋值给x。
减少冗余的参数传递,让代码更简洁。
【2】在Fx函数中实现递归调用
刚学了这个递归调用,我便想在这题中实现一下。
我最初根据题目的要求,在Fx函数中是这么写的:
1 | |
发现无法实现终止递归调用和i的自加,即
graph TD
A[x] --> B[传入Fx()]
B --> C[计算得到new_x]
C --> B
C --> D[判断new_x-x(无法到达)]
style D fill:#fcc,stroke:#f00,stroke-width:1px
卧槽,写到这里我发现我能做到!!!我重新打开visual studio试了一下:
1 | |
在试试如何实现i自加:
代码实现(完整版):
1 | |
注意Fx()调用时的&符号
输入9,程序经过我调试得到:
1 | |
发现i不能自加,我百思不得其解,于是问了问AI:
最核心的原因在于递归调用时传参错误。
❌ 错误原因分析
在你的 Fx 函数定义中,参数 i 本身已经是一个指针 (int* i),它存储的是 main 函数中那个整数变量的地址。
- 第一次调用 (main -> Fx):
main传递了&i。Fx接收到了i的地址。(*i)++成功让main里的i变成了 1。
- 递归调用 (Fx -> Fx):
- 你的代码写的是:
Fx(a, new_x, &i)。 - 错在这里:
i已经在Fx函数里是一个指针变量了。你用了&i,实际上是取了“指针变量自己的地址”(变成了二级指针int\**),而不是“指针指向的地址”。 - 后果: 下一层递归修改的是“上一层函数里那个指针变量的数值”,而不是
main函数里的那个计数器。所以main里的i永远停留在 1(或者第一次修改后的值)。
- 你的代码写的是:
1 | |
💡 图解指针传递的区别
假设 main 里的 i 住在 房间 101。
- 你的写法 (
&i):- 第一层
Fx拿到钥匙:“101”。它去 101 把数字加了 1。 - 递归时,你把第一层手里拿钥匙的这张“纸条的地址”传给了下一层。
- 第二层
Fx拿到的是“纸条的位置”,它去改了“纸条上的字”,而没有去改 房间 101 里的数字。
- 第一层
- 正确写法 (
i):- 第一层
Fx拿到钥匙:“101”。 - 递归时,它把这把钥匙原样复制给下一层。
- 第二层
Fx拿到的还是 “101”,所以大家都在改同一个房间里的数字。
- 第一层
也可以使用全局变量,代码实现:
1 | |
番外
我刚才写递归调用的时候,起初是这么写的:
1 | |
但其实这么写不规范,容易出现大问题
先把递归比作 “传话游戏”
假设:
- 你(主函数
main)要找 “最终答案”(√a 的值); - 递归函数
Fx是 “传话的人”,一共要传 N 次(递归 N 次); - 每次传话的人(第 k 次调用
Fx)要做 3 件事:- 算一个 “新消息”(
new_x); - 把 “新消息” 传给下一个人(第 k+1 次递归),直到有人算出 “最终答案”(满足
fabs(new_x - x) <= EPS); - 拿到 “最终答案” 后,要回头告诉上一个人(函数返回结果),最后传到你手里(主函数
x接收)。
- 算一个 “新消息”(
代码问题:“拿到答案后,不回头传话”
这个代码,第 3 次调用 Fx 就犯了这个错:
- 第 1 次调用(传话人 1):算
new_x=1.5,i=1,传给传话人 2; - 第 2 次调用(传话人 2):算
new_x≈1.4167,i=2,传给传话人 3; - 第 3 次调用(传话人 3):算
new_x≈1.4142,i=3,传给传话人 4; - 第 4 次调用(传话人 4):算
new_x≈1.4142,i=4,发现 “答案对了”(满足终止条件),于是把1.4142回头告诉传话人 3; - 关键问题来了!传话人 3 拿到答案后,没再回头告诉传话人 2:
- 你的代码里,传话人 3(第 3 次调用)在
else分支里,只做了 “接收传话人 4 的答案(给new_x赋值)”,但没做 “把答案传给传话人 2”(没写return new_x); - 就像传话人 3 拿到答案后,站在原地不说话,传话人 2 等不到回复,只能随便编一个 “乱码消息” 传给传话人 1,最后你拿到的就是 “垃圾值”;
- 你的代码里,传话人 3(第 3 次调用)在
举个生活例子更直观
比如:
- 你(主函数)让小明(传话人 1)去问 “最准确的 √2 是多少”,小明问小红(传话人 2),小红问小刚(传话人 3),小刚问小丽(传话人 4);
- 小丽(传话人 4)算出答案是
1.4142,告诉小刚; - 但小刚(传话人 3)拿到答案后,不告诉小红,小红只能瞎编一个
999告诉小明,小明再把999告诉你;
修复方案:“拿到答案后,必须回头传话”
只要在 else 分支里加一句 return new_x,就是让 “传话人拿到答案后,回头告诉上一个人”:
1 | |
这样:
- 传话人 4 告诉传话人 3:答案是
1.4142; - 传话人 3 拿到后,马上告诉传话人 2:答案是
1.4142; - 传话人 2 告诉传话人 1:答案是
1.4142; - 传话人 1 告诉你:答案是
1.4142; - 你拿到正确答案
为什么不加return也可以?
直接运行这个程序,不加return new_x;
1 | |
看看,第4个人告诉第3个人后,第三个人站着发呆,第一个人也能正确得出答案
秘密:C 语言的 “函数返回值残留”(运气成分)
C 语言里,函数的返回值是通过「CPU 的特定寄存器」传递的(比如 x86 架构的eax寄存器)—— 这个寄存器是所有函数共用的 “临时中转站”。
用这个逻辑再看你的代码:
- 传话人 4(第 4 次调用)满足终止条件,执行
return new_x→ 把 1.4142 放进了「eax 寄存器」,然后返回给传话人 3; - 传话人 3(第 3 次调用)接收后,把 1.4142 赋值给
new_x,但没写return→ 函数执行完后,没有主动修改「eax 寄存器」里的值(还是 1.4142); - 传话人 3 返回给传话人 2 时,CPU 默认把「eax 寄存器」里的当前值(1.4142)当成返回值传给传话人 2;
- 传话人 2(第 2 次调用)同样:接收后赋值给
new_x,没写return→ 「eax 寄存器」还是 1.4142,返回给传话人 1; - 传话人 1(第 1 次调用)接收后赋值给
new_x,没写return→ 「eax 寄存器」依然是 1.4142,最终返回给主函数x; - 结果:主函数拿到了正确答案,但这和你的代码逻辑无关,纯粹是「寄存器的值没被覆盖」的巧合!
举个更直观的例子:
就像:
- 小丽(传话人 4)把答案 1.4142 放进了一个 “公共快递柜”(eax 寄存器);
- 小刚(传话人 3)拿到快递后,没重新放进柜子,但也没拿走 / 替换里面的东西;
- 小红(传话人 2)、小明(传话人 1)依次来取,看到柜子里还是 1.4142,就直接拿走了;
- 你(主函数)最终拿到答案,但这不是因为小刚、小红主动传了话,而是他们 “没动快递柜里的东西”。
为什么说这是 “运气好”?(未定义行为的风险)
C 语言标准明确规定:非 void 函数如果没有显式 return,返回值是 “未定义行为”(编译器想怎么处理都可以)。
你的代码能跑通,只是因为:
- VS 编译器在 Debug 模式下,不会主动清空
eax寄存器的值; - 递归过程中,没有其他操作覆盖
eax里的正确答案。
但只要满足以下任何一个条件,答案就会出错 / 程序崩溃:
- 换个编译器(比如 GCC、Clang):有些编译器会在函数结束时,把
eax寄存器清零或设为随机值,返回的就是垃圾值; - 函数里加更多代码:比如在
else分支里加一句printf(会修改eax寄存器),正确答案就被覆盖了; - 递归次数更多:中间某个函数的操作覆盖了
eax,最后返回随机值。
验证:加一句代码,正确答案就没了!
给你的else分支加一句printf(模拟修改寄存器),再运行:
1 | |
运行结果会变成:
1 | |
原因:printf函数执行后,会修改eax寄存器的值(把printf的返回值放进寄存器),覆盖了之前的正确答案 1.4142,最后主函数拿到的就是printf的返回值(或随机值)。
总结
- 你能得到正确答案,是「寄存器残留正确值」的巧合,不是代码逻辑正确;
- 非 void 函数必须在所有分支都写
return—— 这是 C 语言的 “语法铁律”,不写就是未定义行为,今天运气好跑通,明天换环境就可能崩; - 你的代码简化后,正确写法应该是:
1 | |
简单说:「没写 return 还能对」是 C 语言给你开的 “运气挂”,但编程不能靠运气,规范写法才是王道~
【3】实现功能:如果迭代20次之后仍未能达到精度要求,也停止计算。
直接在Fx的定义中添加if终止循环:
代码实现:
1 | |
【4】i 自加时的注意事项
写代码时发现*i++时i无法正常自加
由于++运算符的优先级高于*(解引用运算符),这行代码会被解析为:*(i++)
这导致指针i本身在自增,而不是对它指向的变量(主函数中的i)进行自增。由于指针i指向的地址发生了偏移,后续的自增操作实际上是在操作一个无效的内存地址,最终主函数中的i值没有被正确修改。
需要通过括号强制改变优先级,先解引用指针获取变量,再对变量自增:
即(*i)++
【5】简单且高效的代码:
1 | |
总结:
- 减少冗余的参数传递,让代码更简洁。
- 递归调用的注意事项
- 全局变量的运用