递归调用与指针再探 背景: 依旧是辗转相除法求最大公约数 代码实现:
1 2 3 4 5 6 7 int Gcd (int a, int b) { if (a % b == 0 ) return b; else return Gcd(b, a % b); }
函数 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
这个代码是我自己写的,感觉就是在没逼硬装 我想用新学的递归调用来做这个题的,但是发现递归不是单纯的等于for或者while循环,编程时还遇到了若干函数调用和指针的问题,导致这个程序我大概花了一个小时。总归是写出来了,让我们把这个代码研究清楚吧
我自己写markdown已经一个多月了,也做了很多题目。 在这个过程中,通过不断调试代码和询问AI,学到了很多关于这门语言的知识和相关注意事项。 一有所得我就写下来,并发表在github的个人blog上,我不知道这样学习代码的方法对不对,就我个人而言,我感觉我写代码没天赋,只能努力地去解决一个个问题再记下来
【1】减少冗余的参数传递 在Fx函数的定义中 起初我是这么写的:
1 2 3 4 5 6 7 8 9 float Fx (float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
提示a没有被定义,我才发现还需要把a的初始值传递到封装函数中,于是 修改成最终模样:
1 2 3 x = Fx(a, a, &i); float Fx (float a, float x, int * i)
会发现在Fx函数的定义中,常量a的值是a,float a 的初始值也是a,那么可不可以简化一下,只用传递一个a,将作为x的初始值、计算式中的常量? 可以,修改一下:
1 2 3 4 5 6 7 8 9 10 float Fx (float a, int * i) { float x = a; float new_x; do { new_x = 0.5 * (x + a / x); (*i)++; x = new_x; } while (fabs (new_x - x) >= EPS); return new_x; }
只传递一个a值,并在Fx函数中第一时间把a赋值给x。减少冗余的参数传递 ,让代码更简洁。
【2】在Fx函数中实现递归调用 刚学了这个递归调用,我便想在这题中实现一下。 我最初根据题目的要求,在Fx函数中是这么写的:
1 2 3 4 5 6 7 float Fx (float x) { float new_x = 0.5 * (x + a / x); new_x=Fx(new_x); if (fabs (new_x-x)<=EPS) { return new_x; } }
发现无法实现终止递归调用和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 2 3 4 5 6 7 8 9 10 11 12 13 14 int main{ x = Fx(a, a); } float Fx (float a,float x) { float new_x = 0.5 * (x + a / x); if (fabs (new_x-x)<=EPS) { return new_x; } else { new_x = Fx(a,new_x); return new_x; } }
在试试如何实现i自加: 代码实现(完整版):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a,&i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a,float x,int * i) { float new_x = 0.5 * (x + a / x); (*i)++; if (fabs (new_x-x)<=EPS) { return new_x; } else { new_x = Fx(a,new_x,&i); } }
注意Fx()调用时的&符号 输入9,程序经过我调试得到:
1 2 3 Input a=?9 a=9.000000,x=3.000000,i=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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf ("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = 0.5 * (x + a / x); (*i)++; if (fabs (new_x - x) <= EPS) { return new_x; } else { return Fx(a, new_x, i); } }
💡 图解指针传递的区别 假设 main 里的 i 住在 房间 101 。
你的写法 (&i):
第一层 Fx 拿到钥匙:“101” 。它去 101 把数字加了 1。
递归时,你把第一层手里拿钥匙的这张**“纸条的地址”** 传给了下一层。
第二层 Fx 拿到的是“纸条的位置”,它去改了“纸条上的字”,而没有去改 房间 101 里的数字。
正确写法 (i):
第一层 Fx 拿到钥匙:“101” 。
递归时,它把这把钥匙原样复制 给下一层。
第二层 Fx 拿到的还是 “101” ,所以大家都在改同一个房间里的数字。
也可以使用全局变量,代码实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
0
番外 我刚才写递归调用的时候,起初是这么写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
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,最后你拿到的就是 “垃圾值”;
举个生活例子更直观 比如:
你(主函数)让小明(传话人 1)去问 “最准确的 √2 是多少”,小明问小红(传话人 2),小红问小刚(传话人 3),小刚问小丽(传话人 4);
小丽(传话人 4)算出答案是 1.4142,告诉小刚;
但小刚(传话人 3)拿到答案后,不告诉小红,小红只能瞎编一个 999 告诉小明,小明再把 999 告诉你;
修复方案:“拿到答案后,必须回头传话” 只要在 else 分支里加一句 return new_x,就是让 “传话人拿到答案后,回头告诉上一个人”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
2 这样:
传话人 4 告诉传话人 3:答案是 1.4142;
传话人 3 拿到后,马上告诉传话人 2:答案是 1.4142;
传话人 2 告诉传话人 1:答案是 1.4142;
传话人 1 告诉你:答案是 1.4142;
你拿到正确答案
为什么不加return也可以? 直接运行这个程序,不加return new_x;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
3 看看,第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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
4 运行结果会变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
5 原因:printf函数执行后,会修改eax寄存器的值(把printf的返回值放进寄存器),覆盖了之前的正确答案 1.4142,最后主函数拿到的就是printf的返回值(或随机值)。
总结
你能得到正确答案,是「寄存器残留正确值」的巧合,不是代码逻辑正确;
非 void 函数必须在所有分支都写return—— 这是 C 语言的 “语法铁律”,不写就是未定义行为,今天运气好跑通,明天换环境就可能崩;
你的代码简化后,正确写法应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
6 简单说:「没写 return 还能对」是 C 语言给你开的 “运气挂”,但编程不能靠运气,规范写法才是王道~
【3】实现功能:如果迭代20次之后仍未能达到精度要求,也停止计算。 直接在Fx的定义中添加if终止循环: 代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
7
【4】i 自加时的注意事项 写代码时发现*i++时i无法正常自加 由于++运算符的优先级高于*(解引用运算符),这行代码会被解析为:*(i++) 这导致指针i本身在自增 ,而不是对它指向的变量(主函数中的i)进行自增。由于指针i指向的地址发生了偏移,后续的自增操作实际上是在操作一个无效的内存地址,最终主函数中的i值没有被正确修改。 需要通过括号强制改变优先级,先解引用指针获取变量,再对变量自增: 即(*i)++
【5】简单且高效的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <math.h> #define EPS 1e-5 float Fx (float , float , int *) ;int main () { float a, x; int i = 0 ; printf ("Input a=?" ); scanf_s("%f" , &a); x = Fx(a, a, &i); printf ("\na=%.6f,x=%.6f,i=%d" , a, x, i); return 0 ; } float Fx (float a, float x, int * i) { float new_x = x; do { x = new_x; new_x = 0.5 * (x + a / x); (*i)++; } while (fabs (new_x - x) >= EPS); return new_x; }
8
总结:
减少冗余的参数传递,让代码更简洁。
递归调用的注意事项
全局变量的运用