C 语言里,分号是语句的句号。规则人人都懂:句子写完,分号收尾

但分号其实是”语句终止符”,它并没有规定自己必须站在句尾。于是有了下面这个程序——每个分号都搬家搬到了下一行的行首

用分号作为缩进.c
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>


int main(int argc, char* arg[])
{
;;;;int i = 0
;;;;while(i < 10){
;;;;;;;;printf("%d\n", i)
;;;;;;;;i++
;;;;}
;;;;return 0
;}

第一眼:int i = 0 后面没有分号,这能编译?——能。真机运行,输出干干净净的 0 到 9:

1
2
3
4
5
0
1
2
...
9

原理:把分号从句尾搬到句首

关键在于理解分号到底”属于谁”。C 编译器眼里,换行就是普通的空白,int i = 0 下一行行首的那四个分号,其中第一个分号恰好补上了上一句缺的句号,剩下三个分号是三条合法的空语句(empty statement,什么都不做)。

也就是说,编译器看到的其实是:

1
2
3
4
5
6
7
8
9
10
int i = 0;
;;;
while(i < 10){
;;;;;;;;printf("%d\n", i);
;;;i++;
;;;;
}
;;;
return 0;
}

行首的分号串,一半是补票(终止上一句),一半是空语句(装饰性缩进)。分号从句尾搬到了行首,顺手客串了缩进的角色——这段代码里每个 ;;;; 的数量还刻意不同,嵌套越深分号越多,真就当成缩进用了。

文件末尾还贴心地附上了”正常人版本”的对照注释,两相对比,最有喜剧效果:

1
2
3
4
5
6
7
8
9
// int main(int argc, char* arg[])
// {
// int x = 10;
// while (x-- > 0)
// {
// printf("%d ",x);
// }
// return 0;
// }

(顺便,注释里的 x-- > 0 就是那个著名的”趋向运算符”——下划线完形填空那篇里,这个梗甚至被做成了宏。)

进阶版:整个程序只剩四个分号

同一思想的另一个变体更极端——一个 hello world,把分号省到极致:

抽象-用分号来当分隔符.c
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
;;;;;
;;;;printf("hello world")
;;;;return 0
;;;;
}

真机运行,输出:

1
hello world

逐句拆解这套”分号接力”:第一行 ;;;;; 是五个空语句;第二行 printf("hello world") 没有——它靠下一行行首的第一个分号收尾;return 0 同理,靠再下一行的行首分号收尾。**每一行的行首分号,都在替上一行还债,同时给本行埋下新的债。**整个程序像一场分号的接力赛,最后一棒由 } 前的分号冲线。

正经话

空语句是合法语句,分号归属看词法不看排版——这两条规则都是真的,所以这套写法完全符合标准。顺便说,它的精神续作在隔壁:让下一行行首的分号变成”缩进标记”、配合 while(x --> 0) 的视觉梗,思路一脉相承——都是在玩”分隔符到底分隔了什么“这个最底层的问题。

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