C符号陷阱
术语“符号”指的是程序的一个基本组成单元,其作用相当于一个句子中的单词。在程序中,符号就是程序的一个基本信息单元。而组成符号的字符序列就不同,同一组字符序列在某个上下文环境中属于一个符号,而另一个上下问环境中可能属于完成不用的另一个符号。
1.1 =不同于==
在C语言中符号=作为赋值运算,符号==作为比较,一般而言赋值运算相对于比较运算出现得更频繁,因此字符较少的符号=就被赋予了更常用的含义——赋值操作。此外,在C语言中赋值符号被作为一种操作符对待,因而重复进行赋值操作(如a=b=c)可以很容易地书写,并且赋值操作还可以被嵌入到更大的表达式中。
但是,这种使用上的便利性可能导致一个潜在的问题:当程序员本意是在作比较运算时,却可能无意中误写成赋值运算。比如下例,该语句本意似乎是要检查 x 是否等于 y :
if (x = y) break;
而实际上是将 y 的值赋给了 x ,然后检查该值是否为零。我们再来看看下面的例子:
while (c = ‘ ’ || c == ‘\t’ || c== ‘\n’) c = getc (f);
该程序的本意是跳过文件中的空格符、制表符和换行符,但是由于程序员在比较字符 ' ' 和变量 c 时,误将比较运算符 == 写成了赋值运算符 = 。因为赋值运算符 = 的优先级要低于逻辑运算符 || ,因此实际上是将以下表达式的值赋给了 c :
' ' || c == ‘\t’ || c== ‘\n’
因为 ' ' 不等于零(' ' 的ASCII码值为32),那么无论变量 c 此前为何值,上述表达式的值都是1,因此循环将一直进行下去直到整个文件结束。
某些C编译器在发现形如e1 = e2的表达式出现在循环语句的条件判断部分时,会给出警告消息以提醒程序员。当确实需要对变量进行赋值并检查该变量的新植是否为0时,为了避免来自该类编译器的警告,我们不应该关闭警告选项,而应该显示地进行比较。也就是说:
if (x = y) foo();
应该写作:
if ((x = y) != 0) foo();
这种写法也使得代码的意图一目了然。
前面一直谈的是把比较运算误写成赋值运算的情形,另一方面,如果把赋值运算误写成比较运算,同样会造成混淆:
if ((filedesc == open(argv[i],0)) < 0) error();
在本例中,代码的本意是将函数open的返回值存储在变量filedesc中,然后通过比较变量filedesc是否小于0来检查函数open是否执行成功。但是此处的==本应是 = ,按照上面的代码实际进行的是比较函数open的返回值与变量filedesc,然后检查比较的结果是否小于0。显然比较的结果要么是0,要么是1,永远不可能小于0,所以函数error()将没有机会被调用。
1.2 “贪心法”
C语言对于运算符号有一个简单的规则:每一个符号应该包含尽可能多的字符,也就是说,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分,如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理策略有时被称为“贪心法”。
下面的表达式
a---b
与表达式
a-- - b
的含义相同,而与
a - --b
的含义不同。
根据代码中注释的意思,下面的语句的本意似乎是用x除以p所指向的值,把所得的商再赋给y:
y = x/*p /* p指向除数 */
而实际上 /* 被编译器理解为一段代码注释的开始。应该更清楚点,写作:
y = x / (*p) /* p指向除数 */
这样才得到的实际效果才是语句注释所表示的原意。
练习:1-1. a+++++b 的含义是什么?
上式我们想到有意义的表达就是:(a++) + (++b)
可是,我们根据“贪心法”规则,上式应该被分解为:
((a++)++) + b
但是,a++ 的结果不能作为左值,因此编译器不会接受 a++ 作为后面的 ++ 运算符的操作数,故这样的式子是没意义的。
参考书籍:C陷阱与缺陷