神奇的递归、return、和主函数的定义

C语言学习笔记:利用递归实现字符串倒序输出

作者:王星皓

1. 题目要求

编写一个C语言程序,利用递归函数将输入的字符串倒序输出。

  • 输入提示:input your string:\n

  • 输入格式:%s

  • 输出格式:%c

2. 方法一:使用指针实现 (Pointer Approach)

这是C语言中最常用的递归处理字符串的方法。利用指针算术运算 s+1 逐步向后移动,直到遇到结束符 \0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void reverse_print(char *s) {
if (*s == '\0') {
return; // 基准情况:遇到结束符停止
} else {
reverse_print(s + 1); // 递:移动指针到下一个字符
printf("%c", *s); // 归:打印当前字符
}
}

int main() {
char s[1024];
printf("input your string:\n");
scanf("%s", s);
reverse_print(s);
return 0;
}

3. 方法二:使用数组下标实现 (Index Approach)

如果不熟悉指针或被要求不使用指针运算,可以通过传递一个额外的整型参数 i 作为数组下标来访问字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

// 增加参数 i 表示当前访问的下标
void reverse_print(char s[], int i) {
if (s[i] == '\0') {
return; // 基准情况
} else {
reverse_print(s, i + 1); // 递:下标 + 1
printf("%c", s[i]); // 归:打印 s[i]
}
}

int main() {
char s[1024];
printf("input your string:\n");
scanf("%s", s);
reverse_print(s, 0); // 初始下标为 0
return 0;
}

4. 深入理解:为什么会倒序?(递归的“归”)

在编写递归时,初学者常有的疑问是:“当程序执行到末尾返回时,为什么会回到上一层(例如 i=2)继续执行?”

核心原理:函数调用栈 (Call Stack)

递归并不是单向的“一去不复返”,而是包含“递(压栈)”和“归(出栈)”两个过程。

暂停与等待:当代码执行到 reverse_print(s, i + 1); 时,当前层的函数并没有结束,而是被“暂停”了,它必须等待下一层函数执行完毕。

压栈(Stacking): 每一次调用都会将当前状态压入系统栈中。就像叠盘子一样,i=0 在最底下,i=1 在上面……直到遇到 \0

出栈(Unwinding): 当遇到 \0 触发 return 时,最上面的函数结束,控制权回到下一层(即刚才暂停的地方)。

后进先出 (LIFO): 因为栈是“后进先出”的,所以最后进入的字符(字符串末尾)最先被打印,最先进入的字符(字符串开头)最后被打印。

执行流可视化 (以输入 “abc” 为例)

main 调用 f(0) -> 暂停,等 f(1)

    f(1) 调用 f(2) -> 暂停,等 f(2)

        f(2) ('c') 调用 f(3) -> 暂停,等 f(3)

            f(3) ('\0') -> 遇到基准情况,返回

        回到 f(2) -> 执行 printf('c') -> 结束返回

    回到 f(1) -> 执行 printf('b') -> 结束返回

回到 f(0) -> 执行 printf('a') -> 结束返回

这就是为什么代码看起来是顺序写的,结果却是倒序输出的原因。


希望能帮到正在学习C语言递归的朋友!



我的补充:

这里return的含义

  • 回答: 不是在耍它,这是最标准的“无参返回”。
  • 到底是什么意思: 它的意思就是纯粹的 **“End Execution” (结束执行)**。
  • 场景: 在递归中,这叫 **Base Case (基准情况)**。它的潜台词是:“到了这一步(遇到 \0),不需要再往下递推了,到此为止,回头吧!”

提前终止函数,终止当前函数的执行,像是一个“紧急停止按钮”或者“提前离场通道”

虽然函数声明了不需要返回值,但你依然有权力决定什么时候结束这个函数。

当编译器看到 void 函数里的 return; 时,它生成的汇编指令只是简单的 RET (Return),意味着“清理栈帧,回到上一层去”,它并没有往寄存器里塞任何值

比喻:

  • int 函数的 return 1; 像是你帮表舅买烟,回来后说:“舅,烟买到了(把烟给他)”。
  • void 函数的 return; 像是你表舅让你去关门,你关好后回来只说一句:“好了(没有东西给他,只是告诉他任务结束,流程回到他那里)”。

主函数的return返回问题:

返回非0值

int 的范围通常是 -21亿 到 +21亿。只要在这个范围内,编译器都能接受。

区别: 这是一个约定俗成的规则:

  • return 0; 代表 “程序正常结束,没有错误”
  • return 任何非0值; (比如 1, -1) 代表 “程序异常结束”

eg:

  • main 函数写 return 1;
    (程序关闭,并悄悄给操作系统塞了一张纸条,上面写着“1”)

  • 对你(用户)来说: 毫无影响,你看到了想看的结果。

  • 对操作系统来说: 它认为你的程序“出错了”或者“报警了”。虽然程序跑完了,但系统会在后台记录这个程序是“非正常退出”的。如果你是在 VS 里按 Ctrl + F5 运行,你会看到控制台最后显示:“程序已退出,代码为 1 (0x1)”。(平时是代码为 0)。

给谁看? 这里的返回值是给操作系统(OS)看的。如果你用脚本(比如 Windows 的 .bat 或 Linux 的 Shell)来运行你的程序,脚本可以通过这个返回值判断你的程序是跑通了还是挂了。

返回大于21亿(30亿)的值

  • 会不会报错(编译失败)? 在 Visual Studio 中,不会报错(Error),但会报警告(Warning)。 编译器会提示你:“兄弟,int 装不下这个数啊,我要给你截断了哦”。

  • 会不会运行?能不能倒序输出? 会运行!能输出! 程序不会崩溃,也不会还没打印就直接挂掉。它依然会老老实实把字符串倒序打印出来。

  • 到底发生了什么?(数据溢出) int 类型(通常是32位)最大能表示 2,147,483,647。 当你非要返回 3,000,000,000 时,这叫整数溢出(Overflow)

​ 计算机内部是二进制的,装不下溢出的部分会被直接“切掉”(截断)。 30亿 的二进制大概是这样,前面的位 被切掉后,剩下的二进制会被解释成一个负数

  • 3000000000 (十进制)
  • 10110010110100000101111000000000 (二进制)
  • int 看来,这一串二进制代表的是 -1294967296

后果是什么? 程序正常运行,正常打印,但在退出的一瞬间,给操作系统塞的纸条上写着 -1294967296。 操作系统看着这个负数一脸懵逼,但它也不管,只是记录下这个奇怪的退出代码,然后释放内存。

详细解释计算机是如何通过二进制计算溢出值

1. 这种现象叫什么?

这在计算机里叫 Integer Overflow (整型溢出)。就像汽车的里程表,以前的老车到了 999999 公里,再跑 1 公里就会变成 000000。

但因为 C 语言的 int 是带符号的(Signed),所以它不是归零,而是从最大的正数一下子跳到了最小的负数。

2. 这里的“32个坑位”

现代计算机(Visual Studio 环境下)的 int 通常占用 4个字节,也就是 32个比特位(bit)。

这 32 个坑位,每一位只能填 0 或 1。

  • 第 1 位(最左边): 它是大佬,叫**符号位 (Sign Bit)**。
    • 如果它是 0,代表正数。
    • 如果它是 1,代表负数。
  • 后 31 位: 用来存数值。

3. 案发现场还原

让我们看看 30亿 这个数字。

第一步:把 30亿 变成二进制

3,000,000,000 的二进制写法是这样的:

1011 0010 1101 0000 0101 1110 0000 0000

第二步:把这串二进制硬塞进 int 的 32 个坑位里

编译器把这串数字放进去,突然发现出事了!

你看最左边那一位(高亮):

1011 0010 1101 0000 0101 1110 0000 0000

  • 对于无符号数(Unsigned int): 这一位只是一个普通的数字,这串代码完美代表 30亿。
  • 对于有符号数(int): 计算机看到第一位是 1,它立刻判定:“这是一个负数!”

第三步:计算机开始计算这个负数是谁

计算机使用的是补码(Two’s Complement)规则。

当符号位是 1 时,计算它代表的负数值,规则是:(这串二进制代表的无符号数值)减去 (2的32次方)。

我们来算一下:

  1. 这串二进制代表的无符号数值 = 3,000,000,000 (就是我们要存的30亿)
  2. 2的32次方 (模) = 4,294,967,296 (这是32位能容纳的总数量)

数学公式:

$$3,000,000,000 - 4,294,967,296 = -1,294,967,296$$

真相大白!

这就是为什么你明明给的是 30亿,计算机却给你吐出来一个 -12亿多的负数。

4. 可视化理解(圆环图)

你可以想象 int 的数字排成了一个圆环

  • 圆环的顶点是 0
  • 顺时针走是正数:0, 1, 2 ... 一直走到 21亿 (最大正数)。
  • 逆时针走是负数:0, -1, -2 ... 一直走到 -21亿 (最小负数)。
  • 21亿和-21亿在圆环的底部相连。

当你给了一个 30亿 时,你实际上是在顺时针狂奔。你跑过了 21亿 的界限(最大正数),继续往前跑,就跨过了边界,跑到了负数的领地里去了!

5. 无符号同理吗?

unsigned int 的最大值是 4,294,967,295,那我再加个1,会成负数吗?

不会,而会变成0

想象一个汽车的里程表,或者一个普通的挂钟。

  1. 里程表归零: unsigned int 的最大值是 4,294,967,295。 这就像里程表跑到了 999999
  2. 加 1 操作: 当你给最大值 +1 时,它装不下了。 里程表从 999999 变成了 000000。 所以:4,294,967,295 + 1 = 0
  3. 加 1 再加 1: 4,294,967,296 + 1。 其实这等于 (4,294,967,295 + 1) + 1。 也就是 0 + 1结果是:1

写个代码验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
// 定义无符号最大值
unsigned int max = 4294967295;

printf("最大值: %u\n", max);

// 最大值 + 1
unsigned int overflow1 = max + 1;
printf("最大值+1 (归零): %u\n", overflow1); // 输出 0

// 你问的情况:相当于最大值 + 2,或者说 4294967296 + 1
unsigned int overflow2 = max + 2;
printf("最大值+2 (你的问题): %u\n", overflow2); // 输出 1

return 0;
}

返回其他值

情况一:你写的是 return 'A';(加了单引号)

  • 会怎么样? 程序正常运行,能倒序输出!
  • 为什么?
    • 在 C 语言中,字符(Char)的本质就是一个整数(ASCII 码)。
    • 字符 'A' 在计算机眼里等于整数 65
    • 所以这行代码等同于 return 65;
  • 纸条上写什么? 纸条上写着 65。操作系统觉得没问题,收下了。

情况二:你写的是 return A;(没加单引号)

  • 会怎么样? 直接报错!程序根本无法运行!

    • 你甚至连黑框框(控制台)都看不到,因为还没有生成 .exe 文件。
  • 为什么?

    • 编译器会一脸懵逼地问你:“大哥,A 是个啥?”
    • 它会认为 A 是一个变量名。如果你之前没有定义过 int A = 0;,编译器就不认识它。
    • 结果: 编译器拒绝工作,抛出错误:'A': undeclared identifier(未声明的标识符)。
    • 那个“纸条”呢? 根本没有纸条。因为程序还没生出来这就已经“胎死腹中”了。
  • 我将A提前定义

    不管是提前进行宏定义或者定义全局/局部变量,程序都能正常出生

    比如:当你写 #define A 0 时,这叫宏定义关键点来了: 在编译器真正开始检查语法错误之前,有一个叫预处理器(Preprocessor)的小助手会先跑一遍你的代码。

    它的工作原理就像 Word 文档里的 “查找与替换”

    1. 它发现你定义了 A 代表 0
    2. 它会扫视你的全文,把你代码里所有的 A 统统擦掉,换成 0
    3. 等你真正的编译器(Compiler)来看代码时,代码已经变成了这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 预处理器处理后的样子(编译器看到的真实模样)
    #include <stdio.h>

    // A 已经被替换没了,这里不需要了

    void reverse_print(char s[], int i) {
    // ...省略...
    }

    int main() {
    // ...省略...
    return 0; // 编译器看到的是这个!它根本不知道世界上有个A存在过!
    }

    编译器看到的是 return 0;,它当然非常开心:语法正确,类型匹配,通过!

这就是 C 语言宏定义的魅力。很多大神的 C 语言代码里,为了能让人看懂(比如用 SUCCESS 代替 0,用 ERROR 代替 1),都会大量使用你刚才想到的这一招:

1
2
3
4
5
6
7
#define SUCCESS 0
#define ERROR 1

int main() {
// ...
return SUCCESS; // 这样写,别人读代码时更清楚这是代表“成功”
}

主函数的定义问题

能定义mian为float类型吗?

绝对不行, 标准 C 语言规定 main 必须是 int 类型。


神奇的递归、return、和主函数的定义
http://example.com/2025/12/13/神奇的递归与return/
作者
王柏森
发布于
2025年12月13日
许可协议