今天在网上看到一个输出 hello world!
的C
代码,如下:
#include <stdio.h>
int main(int _)
{
(_ ^ 13) && main(-(~_));
putchar(_["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68);
return 0;
}
测试后确实可以正确输出 hello world!
。
但是为什么呢?让我们来慢慢剖析。
首先看到 main()
有一个参数 _
,其实这个 _
只是一个变量名,要是看不习惯,完全可以改为其他普通变量名称,这里我们就改为最常用的 argc
吧。
关于
main()
函数参数的说明,可以参考这个连接(是不是震惊于main
还要第三个参数?)。
修改变量名后,代码如下:
#include <stdio.h>
int main(int argc)
{
(argc ^ 13) && main(-(~argc));
putchar(argc["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68);
return 0;
}
而在不带参数运行 main()
函数时,argc
的值为 1
,表示有一个参数,即“文件名”。
接下来看 (argc^13) && main(-(~argc));
首先我们需要知道这是一个“与”运算,而“与”运算有一个很重要的特征,那就是“短路”。
那就是说只有前面的 (argc^13)
为真的情况下,才会执行后面的 main(-(~argc))
。
那么 (argc^13)
什么时候为真呢?这是一个“按位异或”运算,当且仅当 argc
也等于 13
时,这个表达式才为假。也就是说,只要 argc
不是 13
,就会执行“与运算”后面的表达式 main(-(~argc))
。
那么 main(-(~argc))
又干了什么呢?很明显,这是一个函数调用,只是这里调用的是 main()
函数,即递归调用,而且这里还带了一个参数。那这个参数计算完后是多少呢?看下表:
argc | (~argc) | -(~argc) |
---|---|---|
1 | -2 | 2 |
2 | -3 | 3 |
3 | -4 | 4 |
… | … | … |
发现规律了吗?
当 argc = 1
时,main(-(~argc))
就相当于 main(2)
;
当 argc = 2
时,main(-(~argc))
就相当于 main(3)
;
…
那么当 argc = 13
时,会发生什么呢?这时候 argc ^ 13
为假,并不会调用 main()
,而是往下执行了!
接下来,终于可以总结第一个语句 (_ ^ 13) && main(-(~_));
的功能了,即不断调用 main(argc)
函数,直到 argc = 13
。既然这样,我是否可以修改一下这个代码,让其更好理解呢?
如下,这个代码和上面的代码执行结果是一样的:
#include <stdio.h>
int cnt = 1;
int main(int argc)
{
while(cnt < 13) {
main(++cnt);
}
putchar(argc["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68);
return 0;
}
看到这里,你能猜到 13
这个数字的意义了吗?没错,就是 hello world!
这个字符串的长度(包括了换行符\n
)。
main()
函数中,递归调用 main()
的那个语句本质上只是调用了一个函数,并没有执行功能;main()
函数的功能,都是由 putchar()
函数完成的。
我们知道,putchar()
函数每次只能输出一个字符,所有要想输出 hello world!\n
就得运行 13
次 putchar()
。
上面的代码,我们可以继续作如下简化:
int main(void)
{
putchar(13["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'h'
putchar(12["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'e'
putchar(11["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'l'
putchar(10["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'l'
putchar(9["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'o'
putchar(8["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // ' '
putchar(7["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'w'
putchar(6["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'o'
putchar(5["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'r'
putchar(4["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'l'
putchar(3["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // 'd'
putchar(2["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // '!'
putchar(1["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68); // '\n'
return 0;
}
到这里,就完全把递归调用 main()
函数的步骤全部去掉了,只需要分析出
putchar(13["~)' #%$&($##!\""][" !$(+.3\335\334\306"-32]+68);
这一个语句就可以了。
分析这个语句之前,我们先看一个简单一点的代码:
#include <stdio.h>
int main(void)
{
int a[3] = {1, 2, 3};
printf("1[a] = %d\n", 1[a]); // 1[a] = 2
printf("1[a-1] = %d\n", 1[a-1]); // 1[a-1] = 1
printf("2[a-2] = %d\n", 2[a-2]); // 2[a-2] = 1
printf("2[a-3] = %d\n", 2[a-3]); // 一个随机值
printf("a[-1] = %d\n", a[-1]); // 一个随机值,但是一定和上面的随机值相等
return 0;
}
亲,发现规律了吗?这里面的关键点就是“数组名”表示的意义是这个数组首个元素的地址,即 a = &a[0];
。
知道这个知识点后,我们再接着来分析 putchar()
语句,我们给它加上一个括号,使各个运算符的优先级更明显:
putchar((13["~)' #%$&($##!\""])[" !$(+.3\335\334\306"-32]+68);
。
那么这里面的 13["~)' #%$&($##!\""]
是个什么鬼呢?根据前面的分析很快可以将其转换成如下代码:
#include <stdio.h>
int main(void)
{
char str[] = "~)' #%$&($##!\"";
putchar(str[13]); // "
putchar(13["~)' #%$&($##!\""]); // "
return 0;
}
字符在 ASCII 码中也就是一个数字, "
对应的十进制数就是 34
。
所以 putchar((13["~)' #%$&($##!\""])[" !$(+.3\335\334\306"-32]+68);
可以再次简化成
putchar(34[" !$(+.3\335\334\306"-32]+68);
。
到这里后是不是已经轻车熟路了?再次把 34[" !$(+.3\335\334\306"-32]
简化后就成了:
#include <stdio.h>
int main(void)
{
char str[] = " !$(+.3\335\334\306";
putchar(str[34-32]); // $
putchar(34[" !$(+.3\335\334\306"-32]); // $
return 0;
}
在 ASCII 表中,$
对应的十进制数是 36
,那么 putchar(36+68)
的输出是什么呢?当然是 h
啦,因为 h
的ASCII 码刚刚好就是 104
,是不是很巧?
要是再把其余的 putchar()
语句也分析一遍,会发现惊人的巧合,输出的就是 hello world!\n
这个字符串,神奇吧~
(废话,本来就是经过巧妙设计的~~)
那么,为什么下面的值是这样呢?
#include <stdio.h>
int main(void)
{
printf("%d\n", '\335'); // -35
printf("%d\n", '\334'); // -36
printf("%d\n", '\306'); // -58
return 0;
}
这时候就该我们win10
自带的计算器上场啦:
这下知道为啥了吧!
是不是以为自己都懂了?那来看看这个?
// 输出编译这个程序时的系统时间
main(_)
{
_^448 && main(-~_);
putchar(--_%64?32|-~(7[(__TIME__)-_/8%8])[">'txiZ^(~z?"-48]>>";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1:10);
}
输出如下图所示: