递归调用与指针再探

递归调用与指针再探

背景:

依旧是辗转相除法求最大公约数

代码实现:

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) {  // 只接收 a(计算用)和 i(次数统计)
float x = a; // 初始迭代值直接在函数内设为 a,不用从外部传
float new_x;
do {
new_x = 0.5 * (x + a / x); // 这里的 a 是参数传递来的(只传了一次)
(*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) { //第一次递归将a传递给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 函数中那个整数变量的地址。

  1. 第一次调用 (main -> Fx):
    • main 传递了 &i
    • Fx 接收到了 i 的地址。
    • (*i)++ 成功让 main 里的 i 变成了 1。
  2. 递归调用 (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);
// 第一次调用传 &i 是对的,因为 main 里的 i 是 int 类型
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 {
// 【修改点 1】 这里直接传 i (因为它已经是地址了)
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
27
28
29
30
#include <stdio.h>
#include <math.h>
#define EPS 1e-5

int i = 0; // 把i改成全局变量,不用指针传递!
float Fx(float, float); // 函数参数去掉int*

int main()
{
float a, x;
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*参数
float new_x = 0.5 * (x + a / x);
i++; // 直接用全局变量i自增,不用解引用!
printf("调试:i=%d, x=%.6f, new_x=%.6f\n", i, x, new_x);
if (fabs(new_x - x) <= EPS) {
return new_x;
}
else {
new_x = Fx(a, new_x); // 递归不用传i
return new_x;
}

番外

我刚才写递归调用的时候,起初是这么写的:

1
2
3
4
5
6
7
8
9
10
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;
}
}

但其实这么写不规范,容易出现大问题

先把递归比作 “传话游戏”

假设:

  • 你(主函数 main)要找 “最终答案”(√a 的值);
  • 递归函数 Fx 是 “传话的人”,一共要传 N 次(递归 N 次);
  • 每次传话的人(第 k 次调用 Fx)要做 3 件事:
    1. 算一个 “新消息”(new_x);
    2. 把 “新消息” 传给下一个人(第 k+1 次递归),直到有人算出 “最终答案”(满足 fabs(new_x - x) <= EPS);
    3. 拿到 “最终答案” 后,要回头告诉上一个人(函数返回结果),最后传到你手里(主函数 x 接收)。
代码问题:“拿到答案后,不回头传话”

这个代码,第 3 次调用 Fx 就犯了这个错:

  1. 第 1 次调用(传话人 1):算 new_x=1.5i=1,传给传话人 2;
  2. 第 2 次调用(传话人 2):算 new_x≈1.4167i=2,传给传话人 3;
  3. 第 3 次调用(传话人 3):算 new_x≈1.4142i=3,传给传话人 4;
  4. 第 4 次调用(传话人 4):算 new_x≈1.4142i=4,发现 “答案对了”(满足终止条件),于是把 1.4142 回头告诉传话人 3;
  5. 关键问题来了!传话人 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
else {
new_x = Fx(a, new_x, i); // 接收下一个人的答案
return new_x; // 把答案回头传给上一个人(关键!)
}

这样:

  • 传话人 4 告诉传话人 3:答案是 1.4142
  • 传话人 3 拿到后,马上告诉传话人 2:答案是 1.4142
  • 传话人 2 告诉传话人 1:答案是 1.4142
  • 传话人 1 告诉你:答案是 1.4142
  • 你拿到正确答案
为什么不加return也可以?

直接运行这个程序,不加return new_x;

1
2
3
Input a=?     2 

a=2.000000,x=1.414214

看看,第4个人告诉第3个人后,第三个人站着发呆,第一个人也能正确得出答案

秘密:C 语言的 “函数返回值残留”(运气成分)

C 语言里,函数的返回值是通过「CPU 的特定寄存器」传递的(比如 x86 架构的eax寄存器)—— 这个寄存器是所有函数共用的 “临时中转站”。

用这个逻辑再看你的代码:

  1. 传话人 4(第 4 次调用)满足终止条件,执行return new_x → 把 1.4142 放进了「eax 寄存器」,然后返回给传话人 3;
  2. 传话人 3(第 3 次调用)接收后,把 1.4142 赋值给new_x,但没写return → 函数执行完后,没有主动修改「eax 寄存器」里的值(还是 1.4142);
  3. 传话人 3 返回给传话人 2 时,CPU 默认把「eax 寄存器」里的当前值(1.4142)当成返回值传给传话人 2;
  4. 传话人 2(第 2 次调用)同样:接收后赋值给new_x,没写return → 「eax 寄存器」还是 1.4142,返回给传话人 1;
  5. 传话人 1(第 1 次调用)接收后赋值给new_x,没写return → 「eax 寄存器」依然是 1.4142,最终返回给主函数x
  6. 结果:主函数拿到了正确答案,但这和你的代码逻辑无关,纯粹是「寄存器的值没被覆盖」的巧合!
举个更直观的例子:

就像:

  • 小丽(传话人 4)把答案 1.4142 放进了一个 “公共快递柜”(eax 寄存器);
  • 小刚(传话人 3)拿到快递后,没重新放进柜子,但也没拿走 / 替换里面的东西;
  • 小红(传话人 2)、小明(传话人 1)依次来取,看到柜子里还是 1.4142,就直接拿走了;
  • 你(主函数)最终拿到答案,但这不是因为小刚、小红主动传了话,而是他们 “没动快递柜里的东西”。
为什么说这是 “运气好”?(未定义行为的风险)

C 语言标准明确规定:非 void 函数如果没有显式 return,返回值是 “未定义行为”(编译器想怎么处理都可以)。

你的代码能跑通,只是因为:

  • VS 编译器在 Debug 模式下,不会主动清空eax寄存器的值;
  • 递归过程中,没有其他操作覆盖eax里的正确答案。

但只要满足以下任何一个条件,答案就会出错 / 程序崩溃:

  1. 换个编译器(比如 GCC、Clang):有些编译器会在函数结束时,把eax寄存器清零或设为随机值,返回的就是垃圾值;
  2. 函数里加更多代码:比如在else分支里加一句printf(会修改eax寄存器),正确答案就被覆盖了;
  3. 递归次数更多:中间某个函数的操作覆盖了eax,最后返回随机值。
验证:加一句代码,正确答案就没了!

给你的else分支加一句printf(模拟修改寄存器),再运行:

1
2
3
4
5
6
7
8
9
10
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);
printf("加一句打印\n"); // 新增:修改eax寄存器
}
}

运行结果会变成:

1
2
3
4
5
6
Input a=?2
加一句打印
加一句打印
加一句打印

a=2.000000,x=0.000000 // 答案变成垃圾值!

原因:printf函数执行后,会修改eax寄存器的值(把printf的返回值放进寄存器),覆盖了之前的正确答案 1.4142,最后主函数拿到的就是printf的返回值(或随机值)。

总结
  1. 你能得到正确答案,是「寄存器残留正确值」的巧合,不是代码逻辑正确;
  2. 非 void 函数必须在所有分支都写return—— 这是 C 语言的 “语法铁律”,不写就是未定义行为,今天运气好跑通,明天换环境就可能崩;
  3. 你的代码简化后,正确写法应该是:
1
2
3
4
5
6
7
float Fx(float a,float x) {
float new_x = 0.5 * (x + a / x);
if (fabs(new_x-x)<=EPS) {
return new_x;
}
return Fx(a, new_x); // 直接返回递归结果,不用else和中间赋值
}

简单说:「没写 return 还能对」是 C 语言给你开的 “运气挂”,但编程不能靠运气,规范写法才是王道~

【3】实现功能:如果迭代20次之后仍未能达到精度要求,也停止计算。

直接在Fx的定义中添加if终止循环:

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
float Fx(float a, float x, int* i) {
float new_x = x;
do {
x = new_x;
new_x = 0.5 * (x + a / x);
(*i)++;
if (*i >= 20) {
break;
}
} while (fabs(new_x - x) >= EPS);
return new_x;
}

【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 // 精度要求:0.00001
#define MAX_ITER 20 // 最大迭代次数

int main() {
float a, x, new_x;
int iter = 0; // 迭代次数

printf("Input a=?");
scanf("%f", &a); // 标准输入函数,兼容性更好

x = a; // 初始值x0=a
do {
new_x = 0.5 * (x + a / x); // 牛顿迭代公式
iter++; // 迭代次数加1
if (iter >= MAX_ITER) {
break; // 超过20次迭代则停止
}
x = new_x; // 更新x为新迭代值
} while (fabs(new_x - x) >= EPS); // 精度未满足则继续迭代

printf("\na=%.6f,x=%.6f,i=%d", a, new_x, iter);
return 0;
}

总结:

  1. 减少冗余的参数传递,让代码更简洁。
  2. 递归调用的注意事项
  3. 全局变量的运用


递归调用与指针再探
http://example.com/2025/11/20/递归调用与指针再探/
作者
王柏森
发布于
2025年11月20日
许可协议