神奇的递归、return、和主函数的定义
C语言学习笔记:利用递归实现字符串倒序输出
作者:王星皓
1. 题目要求
编写一个C语言程序,利用递归函数将输入的字符串倒序输出。
输入提示:input your string:\n
输入格式:%s
输出格式:%c
2. 方法一:使用指针实现 (Pointer Approach)
这是C语言中最常用的递归处理字符串的方法。利用指针算术运算 s+1 逐步向后移动,直到遇到结束符 \0。
1 | |
3. 方法二:使用数组下标实现 (Index Approach)
如果不熟悉指针或被要求不使用指针运算,可以通过传递一个额外的整型参数 i 作为数组下标来访问字符。
1 | |
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次方)。
我们来算一下:
- 这串二进制代表的无符号数值 = 3,000,000,000 (就是我们要存的30亿)
- 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
想象一个汽车的里程表,或者一个普通的挂钟。
- 里程表归零:
unsigned int的最大值是4,294,967,295。 这就像里程表跑到了999999。 - 加 1 操作: 当你给最大值
+1时,它装不下了。 里程表从999999变成了000000。 所以:4,294,967,295 + 1 = 0。 - 加 1 再加 1:
4,294,967,296 + 1。 其实这等于(4,294,967,295 + 1) + 1。 也就是0 + 1。 结果是:1。
写个代码验证一下
1 | |
返回其他值
情况一:你写的是 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 文档里的 “查找与替换”:
- 它发现你定义了
A代表0。 - 它会扫视你的全文,把你代码里所有的
A统统擦掉,换成0。 - 等你真正的编译器(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 | |
主函数的定义问题
能定义mian为float类型吗?
绝对不行, 标准 C 语言规定 main 必须是 int 类型。