最近在一个论坛发现了一段简单的C程序很有意思,其中蕴含着不少技巧,在此与大家分享一下。

原程序如下:

1
2
#include <stdio.h>
main(_){char*x="*b#**000**I#*******2*0***#-.****5.*-#-.****54.#*******2**6#****00**0.#";while(_=*x/4)_-=8,printf("\n%*s"+!!_,_+_,"_/_/_/"+*x++%4*2);}

表面看起来很奇怪的代码,我把它放到VC++6下面编译,发现不能通过,提示“_”未声明,对以上代码稍加修改,如下:

1
2
3
#include <stdio.h>

main(int _){char*x="*b#**000**I#*******2*0***#-.****5.*-#-.****54.#*******2**6#****00**0.#";while(_=*x/4)_-=8,printf("\n%*s"+!!_,_+_,"_/_/_/"+*x++%4*2);}

细心的人已经看出来了,就是在main函数的参数“_”前面加上了变量的类型:int。此时,以上代码在VC6中就能够正常的编译和连接了。运行的结果如下:

  _/                              _/
  _/  _/  _/_/_/  _/_/_/  _/_/_/  _/  _/                _/_/
  _/  _/  _/  _/  _/  _/  _/      _/  _/  _/_/_/  _/  _/  _/
  _/_/    _/  _/  _/  _/  _/      _/_/    _/  _/  _/_/
  _/_/    _/  _/  _/  _/  _/      _/_/    _/_/_/    _/
  _/  _/  _/  _/  _/  _/  _/      _/  _/  _/        _/
  _/  _/  _/  _/  _/_/_/  _/_/_/  _/  _/  _/_/_/    _/

是用“_/”组成的单词“knocker”。

下面我们来分析这个小小的程序,不过为了看起来直观,我把这个程序稍加改动:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

main(int _) {
  char*x="*b#**000**I#*******2*0***#-.****5.*-#-.****54.#*******2**6#****00**0.#";
  while(_=*x/4) {
    _-=8;
    printf("\n%*s"+!!_,_+_,"_/_/_/"+*x++%4*2);
  }
}

首先来看看main函数参数,这个参数的名称比较奇怪“_”,我们看着有些不习惯,但它确实是合法的变量名称。另外,本来我们常见的main函数一般都不带参数,如果有参数是因为程序希望处理命令行格式下运行这个程序所需要的参数,一般是这样的:main(int argc, char **argv),第一个参数argc表示参数表中参数的个数,argc是一个char型二维数组,保存着参数字符串。

举个例子,如果我们在命令行下输入命令:dir –s c:\。此时,argc的值为3(包括命令本身),argv[0]指向字符串:dir,arg[1] 指向字符串:-s,arg[2]指向字符串“c:\”。那么既然main函数的参数通常要不没有,要不就是两个,而这个程序只有一个,这样做是否合法呢?答案是肯定的!我在《关于C语言中的变量》中提到过,函数中的参数是保存在堆栈中的,所以这就涉及到一个谁来平衡堆栈的问题,是调用者还是被调用者。在使用VC6编译器的时候,如果函数没有特别的声明,默认是调用者清理堆栈。换句话说,运行时库(runtime)调用main函数的时候,只传递一个参数给main函数,这一点它自己是知道的,当main函数调用结束以后,它在平衡堆栈的时候,只清除掉一个函数,因此不会出现任何问题。

我们接着往下看,下面定义了一个char型指针变量x,指向一个字符串。接下来是while语句,while(_=(*x)/4),当表达式_=(*x)/4的值不为0的时候就执行while的循环体,然而表达式_=(*x)/4是一个赋值语句,它的值又是多少呢?在这种情况下,通常赋值号右面的表达式的值就是整个表达式的值。第一次执行这个语句的时候,x指向字符‘*’,对应的值为42,以此类推,当x=0时,也就是字符串结束的时候,循环结束。

接下来在原来的程序中是:_-=8,printf("\n%*s"+!!_,_+_,"_/_/_/"+*x++%4*2);这里的逗号作用有些类似分号,但是逗号两侧组成的是一条语句,而分号则是两条语句。

我们仔细看printf这条语句,它的格式字符串为:“\n%*s”,其中%*s很少见,很多人不知道这是什么格式,不过我们可以在MSDN中找到这样的描述:

If the width specification is an asterisk (*), an int argument from the argument list supplies the value. The width argument must precede the value being formatted in the argument list.

意思是说如果宽度用星号(*)来指定,则应该在参数列表中提供一个int型参数作为宽度的值。这样就清楚了,原来这个printf语句是用表达式_+_来控制字符串”%s”的宽度,也就是替换其中的()。

在仔细一看,原来格式字符串还没有完!完整的应该是:"\n%*s"+!!_。这就奇怪了,字符串怎么和!!_相加呢?其实也不奇怪,在这里!!_是对变量_作了两次“非”的操作,结果应该是0或1。要明确的是,字符串作为参数传递给函数的时候,只是把字符串的首地址传递给了函数,所以字符串"\n%*s"的首地址就是字符‘\n’的地址,当这个地址加1的时候,传递给printf函数的格式字符串就变成了"%*s",所以这个+!!_的奥妙就在于控制换行!是不是很有创意?

当然了,下面的代码同样有创意,就是printf语句要打印的字符串参数:"_/_/_/"+*x++%4*2。经过刚才的分析,我们不难理解"_/_/_/"加上后面的表达式的用意了,同样是控制输出字符的个数,是一个“_/”,两个,还是三个。那么关键就是后面这个表达式了:*x++%4*2。其实慢慢的分析也不困难,先执行*x%4*2,然后执行x++。所以,当在执行while语句的时候,x会指向下一个字符!

综上所述,这个小程序就是利用一个字符串来控制输出的例子,里面运用了很多技巧,对于初学者需要很好的理解,对今后的编程是很有帮助的!