一段有趣的C程序(续)

同学看完我的一段有趣的程序后,给了我一段bt的程序:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

俺就又本着人不bt枉少年的精神,又bt的研究了一下。程序贴到vc里,编译出现三个错误,一个是printf未定义,一个是F_00未定义。改动后如下,同时将程序结构改的可读性更强:

#include<stdio.h>
#define _ -F<00||--F-OO--;
F_OO();

int F=00,OO=00;

main(){
    F_OO();
    printf("%1.3f\n",4.*-F/OO/OO);
}

F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

首先,这个程序bt到用00和OO近似来混淆视听!OO是两个大写字母o,可以作为变量名,而00单独写只是数字0,不能作为变量名。

程序首先定义了一段宏_(md,又是这个下划线),之后定义了两个变量F和OO,并赋予初值0。这里要注意,宏里使用到的就是这两个变量。

之后定义了一个函数F_00(),大量调用了这个宏,来改变变量F和OO的值。虽然程序看起来bt,但实际上归结起来只有两种语句“_”和“-_”两种。这两语句在进行宏替换后分别为“-F<00||--F-OO--;”和“--F<00||--F-OO--;

语句“-F<00||--F-OO--;”根据优先级判断,先执行||左边的部分,由于F=0,所以左部分为假(0<0为假),在执行右半部分,先对F自减1,执行F-OO,再对OO自减1。最后的结果为-1,再进行||,舍弃最后结果。折腾半天,只有对F和OO的自减使变量发生了变化,最终的运算结果并不care。

语句“--F<00||--F-OO--;”根据优先级判断,先执行||左边部分,对F自减1后判断是否F<0。经过上个语句后,F值为-1,因此F<0为真,不再执行右半部分。

但是注意!*在单步跟踪“-_”语句时,变量的值没有发生任何变化!*也就是说刚才的分析并不是实际情况。由于变量值没有任何改变,可以肯定||右边的部分根本没有被运行,也就是说||左边的部分结果为真。如果左边的语句解释为“- -F<00”(注意两个减号中间的空格),则F的值没有被改变,且其值为真。因此,这里的“-_”语句实际是被解释成“- -F<00||--F-OO--;”,也就是“(-(-F))<00||--F-OO--;”。

由于“-_”没有对变量的值进行任何操作,因此函数F_00()里看似唬人的大球,实际每行只有行首的“_”语句起作用,是对两个变量的值自减1。因此,整个高度为16的球,实际对两个变量进行16次自减1。真是无聊!!!!!

主程序就简单多了。首先运行F_00()改变两个变量的值,最后运行printf("%1.3f\n",4.*-F/OO/OO)语句输出。这个printf语句就简单了,输出一个长度为1,3位小数宽度的浮点数并换行。因为浮点的总长度肯定大于1,所以相当于输出一个3位小数宽的浮点并换行。输出的浮点内容为“4.*-F/OO/OO”,也就是“(4.)*(-F)/OO/OO”,此时F和OO的值都是“-16”,结果很显然,没啥大意思了。

前面所讲到的问题,应该涉及到c语言编译器实现的细节问题。c语言编译器指对宏进行简单替换,而不进行任何语法判断,因此可以肯定,对宏的替换工作是先于语法判断和编译过程的。c编译器在进行语法判断时,使用的是大嘴原则,也就是尽可能将更多的字符解释为运算符等关键字,而实际中并没有将“-_”解释为“--F<00||--F-OO--;”,可以认为,编译器在替换前,会对宏的前后进行标识,将宏本身与前后的语句分开,这样,宏本身的运算符就不会与程序前后的运算符关联在一起,改变宏语句本身的运算符的意义。因此,这个程序中的宏最终解释为“- -F<00||--F-OO--;”。

另一个值得注意的问题是,“-_”在解释为“- -F<00||--F-OO--;”之后,实际宏本身的运算顺序已经改变。这里也是为什么在《c陷阱与缺陷》里,作者特别提到,宏的定义应该加括号,格式为“#define NN (…)”,以免程序中出现很隐蔽的,改变宏本身运算顺序的逻辑错误。

为了验证编译器对宏的替换过程,编写了下面的程序:

#define A ++a
void main(void){
 int a=0;
 a=a+++A;
}

实际程序运行通过,证明“a=a+++A”被解释为“a=a+++ ++a”即“a=(a++)+(++a)”,而不是“a=a+++++a”。其中后一种译法会因为大嘴原则译为“a=((a++) ++)+a”第二层的“++”会因为左边不是一个有效变量,而引起编译错误:error C2105: '++' needs l-value

Googol Lee

多年生软件工程师,信仰开源

Munich, Germany http://air.googol.im