【C 陷阱与缺陷 】(一)词法陷阱

一 内容

0. =不同于==

当程序员本意是作比较运算时,却可能无意中误写成了赋值运算。

1.本意是检查 x 与 y 是否相等:

if(x = y)
break;

实际上是将 y 的值赋值给了 x ,然后再检查该值是否为 0 。

2.本意是跳过文件中的空白字符:

while(c = '' || c == '\t' || c == '\n')
c = getc(f);

因为 ' '不等于 0 (' '的 ASCII 码值为 32),那么无论变量为何值,上述表达式求值的结果都为 1,因此循环将进行下去直到整个文件结束。

C 编译器发现形如 x = y 的表达式出现在选择语句,循环语句的条件判断部分时,会给出警告。当确实需要对变量进行赋值时,为了避免警告,我们应该这样处理:

if((x = y) != 0)
foo();

如果将赋值写成了比较,也会造成混淆:

if((filedesc == open(argv[i], 0)) < 0)
error();

本例中,open 执行成功返回非零值,失败返回 -1。本意是将 open 函数的返回值存储在变量 filedesc 中,然后将其和 0 比较大小,判断 open 执行是否成功 。==运算符的结果只可能是 1 或 0,永远不会小于 0,所以 error() 将没有机会被调用。

1. &|不同于&&||

比较 i & ji && j ,只要 i 和 j 是 0 或 1 ,两个表达式的值是一样的(||| 同理。)。然而,一旦 i 和 j 的值为其他,两个表达式的值不会始终一致。

另一个区别是操作数带有自增自减的运算:

i & j++, j 始终会自增;但是 i && j++ 有时 j 不会自增。

2. 词法分析中的“贪心法”

当 C 的编译器读入一个字符/后跟着一个字符*时,那么编译器就必须做出判断:时将其作为两个符号对待,还是合起来作为一个符号对待。这类问题的规则:每个符号应该包含尽可能多的符号

例如:a---b(a--) - b含义相同,而与a - (--b)含义不同。

又如:下面的语句本意是 x 除以 p 指向的值然后将结果赋值给 y

y = x/*p;

但是,实际上 /*被编译器理解为一段注释的开始。

将上面的语句重写如下:

y = x / *p;

或者:

y = x/(*p);

老版本的编译器允许使用=+来代表现在+=的含义,这种编译器会将:

a=-1;

理解为:

a =- 1;

即为:

a = a - 1;

因此,如果程序员的原意为:

a = -1;

那么结果会让其大吃一惊。

再如:

a=/*b;

在老版本的编译器会将其当作:

a =/ *b;

3. 整型常量

许多编译器会把 8 和 9 作为把八进制的数字处理,这种处理方式来源于八进制数的定义。例如:0195 的含义是1x8^2 + 9x8 + 5x8^0也就是 141(十进制)或 0215(八进制)。ANSI C 标准中禁止这种用法。

4. 字符与字符串

单引号引起的一个字符实际上代表一个整数。整数值对应于该字符在编译器采用的字符集中的序列值。因此,对于采用 ASCII 字符集的编译器而言,'a'的含义与 97 (十进制)严格一致。

用双引号引起的字符串,代表的确实一个指向无名数组起始字符的指针。该数组被双引号之间的字符以及一个额外的二进制值为 0 的字符\0初始化。

比如,下面的这个语句:

printf("Hello World\n");

等价于:

char hello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\n', 0};
printf(hello);

整数型(一般为 16 或 32 位)的存储空间可以容纳多个字符(一般为 8 位),因此有的编译器允许在一个字符常量(以及字符串常量)中包含多个字符。也就是说:用'yes'代替"yes"不会被该编译器检测到。前者的含义大多数编译器理解为一个整数值,由'y','e','s'所代表的整数值按照特定编译器实现中的定义方式组合得到。

二 练习

练习 1

某些 C 编译器允许嵌套注释。请写一个测试程序,要求:无论编译器是否允许嵌套注释,该程序都能正常通过编译,但是两种情况下程序执行结果不同。

对于符号序列:

/*/**/"*/"

如果允许嵌套注释,上面的符号序列表示:一个单独的双引号",因为最后的注释符前出现的符号都会被当作注释的一部分。

如果不允许嵌套注释,上面的符号就表示一个字符串:"*/"

Doug Mcllroy 发现了下面这个令人拍案叫绝的解法:

/*/*/0 */**/1

这个解法主要利用了编译器作词发分析时的“贪心法”规则。

如果编译器允许嵌套注释,则将上式解释为:

/* /*/0 */ * */ 1

上式的值为 1

如果编译器不允许嵌套注释,则解释为:

/* / */ 0 * /**/ 1

也就是 0*1,值为 0

练习 2

a+++++b 的含义是什么?

上式唯一有意义的解析方式就是:

a++ + ++b

可是,根据“贪心法”的规则,上式应该被解释为:

a++ ++ + b

等价于:

(a++)++ + b;

但是 a++的值不能作为左值,因此编译器不会接受 a++ 作为后面 ++ 运算的操作数。

参考资料《C 缺陷与陷阱》