先上代码。下面是一个完整的、可以编译、可以运行的 C 语言程序,它是一个从 9 倒数到 0 的倒计时器。请欣赏:

_.c
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>

#define _ int
#define __ main
#define ___ while
#define _____ x
#define ______ ;
#define _______ ()
#define ________ {
#define _________ }
#define __________ (
#define ___________ )
#define ____________ =
#define _____________ -->
#define ______________ "%d\n",
#define _______________ printf
#define ________________ return

_ __ _______
________
_ _____ ____________ 10 ______
___ __________ _____ _____________ 0 ___________
________
_______________ __________ ______________ _____ ___________ ______
_________
________________ 0 ______
_________

如果你没有关掉这篇文章,说明你的好奇心还在。这个东西确实是 C 语言,而且用的不是乱码、不是 Unicode 花活,是从头到尾老老实实的 ASCII 下划线。

原理:一个宏就是一次填空

答案就藏在文件开头那串 #define 里——这不是代码,这是一张完形填空的答案表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _ int
#define __ main
#define ___ while
#define _____ x
#define ______ ;
#define _______ ()
#define ________ {
#define _________ }
#define __________ (
#define ___________ )
#define ____________ =
#define _____________ -->
#define ______________ "%d\n",
#define _______________ printf
#define ________________ return

C 语言允许你把任何东西定义成宏,关键字也不例外。于是:

  • _ 变成 int
  • __ 变成 main
  • ___ 变成 while
  • ______ 变成分号
  • ________ 变成左花括号
  • ……

一条下划线是 int,两条是 main,三条是 while——下划线的数量就是填空的题号。拿着答案表回头看那个程序,逐行”对答案”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_ __ _______
→ int main()

_ _____ ____________ 10 ______
→ int x = 10 ;

___ __________ _____ _____________ 0 ___________
→ while ( x --> 0 )

_______________ __________ ______________ _____ ___________ ______
→ printf ( "%d\n", x ) ;

________________ 0 ______
→ return 0 ;

它是一个倒计时器:从 9 打印到 0。真机运行,输出干干净净:

1
2
3
4
5
6
9
8
7
...
1
0

彩蛋:连 –> 梗都被做进了宏里

拆到 while 那一行,老 C 语言玩家会会心一笑:x --> 0——这不是什么”趋向运算符”,它是 x-- > 0 被空格美化后的样子,是 C 语言圈流传最广的视觉梗。而这个程序走得更远:--> 直接定义成了宏 _____________。也就是说在这个程序的世界观里,--> 真的是一种合法运算符——只要你觉得它是,宏就让它合法。

怎么复原?填空题的逆操作

看到这里你可能会问:如果我接手了别人这种代码,怎么还原?答案简单到离谱——预处理器的本职工作就是展开宏,让编译器替你”对答案”就行:

1
gcc -E -o a.i a.c

-E 表示只跑预处理阶段。打开输出的 a.i,所有宏已经被展开,下划线变回 intmainwhile,一行不剩。当时我在文件夹里留了一个 复原代码.txt,全文就这一行命令。

这个系列的其他文章:尊嘟假嘟会自杀的代码。系列缘起见抽象代码宣言