![C陷阱与缺陷](https://wfqqreader-1252317822.image.myqcloud.com/cover/22/35243022/b_35243022.jpg)
1.1 =不同于==
由Algol派生而来的大多数程序设计语言,例如Pascal和Ada,以符号:=作为赋值运算符,以符号=作为比较运算符。而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,所以循环将一直进行下去,直到整个文件结束。文件结束之后循环是否还会进行下去,要取决于getc库函数的具体实现,即该函数在文件指针到达文件结尾之后是否还允许继续读取字符。如果允许继续读取字符,那么循环将一直进行,从而成为一个死循环。
某些C编译器在发现形如e1 = e2的表达式出现在循环语句的条件判断部分时,会给出警告消息以提醒程序员。当确实需要对变量进行赋值并检查该变量的新值是否为0时,为了避免来自该类编译器的警告,我们不应该简单关闭警告选项,而应该显式地进行比较。也就是说,下例
if (x = y)
foo();
应该写作:
if ((x = y) != 0)
foo();
这种写法也使得代码的意图一目了然。至于为什么要用括号把x = y括起来, 2.2节将讨论这个问题。
前面一直谈的是把比较运算误写成赋值运算的情形,此外,如果把赋值运算误写成比较运算,同样会造成混淆:
if ((filedesc == open(argv[i], 0)) < 0)
error();
在本例中,如果函数open执行成功,将返回0或者正数;而如果函数open执行失败,将返回−1。上面这段代码的本意是将函数open的返回值存储在变量filedesc之中,然后通过比较变量filedesc是否小于0来检查函数open是否执行成功。但是,此处的==本应是=。而按照上面代码中的写法,实际进行的操作是比较函数open的返回值与变量filedesc。然后检查比较的结果是否小于0,因为比较运算符==的结果只可能是0或1,永远不可能小于0,所以函数error()将没有机会被调用。如果代码被执行,似乎一切正常,除了变量filedesc的值不再是函数open的返回值(事实上,甚至完全与函数open无关)。某些编译器在遇到这种情况时,会警告与0比较无效。但是,程序员不能指望靠编译器来提醒,毕竟警告消息可以被忽略,而且并不是所有编译器都具备这样的功能。