先上完整代码。这是原版原样,一个字都没改——它编译零警告,运行也正常退出。你能看出它输出了什么吗?

坑人的c语言.c
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
int i = 0;
for(i = 0; i < 114514; ++i) {i=114514;}
printf("%d ", i);
return 0;
}

直觉上,这是一个从 0 数到 114513 的循环,printf 在循环里刷屏。真机编译运行,实际输出是:

1
114515

对,你没有看错。不刷屏、不是 114514、甚至不是从 0 开始——整个”循环”只吐出了一行孤零零的 114515,而且这个数还比代码里的 114514 多了 1。哪里出问题了?

提示:这段代码是可以横向滚动的。把代码框的滚动条拖到最右边,或者点一下工具栏新加的「自动换行」按钮开启折行——答案就在眼前。

原理:看不见的循环体

现在揭晓。for 那一行的行尾和 {i=114514;} 之间,隔着约 600 个连续的空格——上面的代码块是原版原样,一个空格都没删。把这段空白折叠掉,程序的真实结构是:

1
2
for(i = 0; i < 114514; ++i) {i=114514;}
printf("%d ", i);

花括号和 for 在同一行——它就是循环体。而下一行缩进着的 printf,才是循环外的孤代码。整个程序的执行过程其实是:

  1. 进入循环,循环体把 i 直接顶到 114514
  2. i++,变成 114515
  3. 条件 114515 < 114514 不成立,退出;
  4. printf 打印 114515——这就是那行孤零零的输出,比 114514 多 1 是因为 ++i 先走了一步。

至于为什么”看不见”:几百个空格在编辑器里渲染成一大段空白地带,{i=114514;} 被推到屏幕极远处,肉眼扫代码时基本等于不存在——而下一行带着缩进的 printf 看上去才是循环体。视觉上花括号是循环外的一句孤代码,语法上它才是循环体本身printf 看似在循环里,实际只执行一次。

它是怎么骗到人的

这个程序的杀伤力在于三个错觉叠加:

  1. 缩进错觉printf 前面的缩进,让大脑默认”这句在循环里”——但 C 语言里缩进不参与语法,花括号才作数;
  2. 空白错觉:几百个空格在大多数编辑器里渲染成”一段空白”,看起来像空行,实际上不是;
  3. 输出错觉:看到 114515 的第一反应是”程序坏了”或”随机数吧”,很少有人立刻想到循环只跑了一次。

正经话

这段代码的教训一句就能说完:花括号定边界,缩进只负责骗人。C 语言的语法归属看的是花括号和分号,不是缩进和换行——这也是 Python 程序员刚接触 C 时最容易翻车的地方(Python 缩进即语法,C 缩进即艺术)。审查别人代码的时候,别信排版,信括号。

系列其他文章见抽象代码宣言