本节书摘来自异步社区《C陷阱与缺陷》一书中的第1章,第1.1节,作者 【美】Andrew Koenig,更多章节内容可以访问云栖社区“异步社区”公众号查看
C陷阱与缺陷当我们阅读一个句子时,我们并不去考虑组成这个句子的单词中单个字母的含义,而是把单词作为一个整体来理解。确实,字母本身并没有什么意义,我们总是将字母组成单词,然后给单词赋予一定的意义。
对于用C语言或其他语言编写的程序,道理也是一样的。程序中的单个字符孤立来看并没有什么意义,只有结合上下文才有意义。因此,在p->s = "->";这个语句中,两处出现的'-'字符的意义大相径庭。更精确地说,上式中出现的两个'-'字符分别是不同符号的组成部分:第一个'-'字符是符号->的组成部分,而第二个'-'字符是一个字符串的组成部分。此外,符号->的含义与组成该符号的字符'-'或字符'>'的含义也完全不同。
术语“符号”(token)指的是程序的一个基本组成单元,其作用相当于一个句子中的单词。从某种意义上说,一个单词无论出现在哪个句子,它代表的意思都是一样的,是一个表义的基本单元。与此类似,符号就是程序中的一个基本信息单元。而组成符号的字符序列就不同,同一组字符序列在某个上下文环境中属于一个符号,而在另一个上下文环境中可能属于完全不同的另一个符号。
译注:
如上面的字符'-'和字符'>'组成的字符序列->,在不同的上下文环境中,一个代表->运算符,一个代表字符串"->"。编译器中负责将程序分解为一个一个符号的部分,一般称为“词法分析器”。
再看下面一个例子,语句:
if (x > big) big = x;这个语句的第一个符号是C语言的关键字if,紧接着下一个符号是左括号,再下一个符号是标识符x,再下一个是大于号,再下一个是标识符big,依次类推。在C语言中,符号之间的空白(包括空格符、制表符或换行符)将被忽略,因此上面的语句还可以写成:
if ( x > big ) big = x ;本章将探讨符号和组成符号的字符间的关系,以及有关符号含义的一些常见误解。
由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比较无效。但是,作为程序员不能指望靠编译器来提醒,毕竟警告消息可以被忽略,而且并不是所有编译器都具备这样的功能。
相关资源:C陷阱与缺陷 第二版 高清扫描版