3.3 谁是谁的谁

简单的表达式求值比较容易,根据运算符的定义就可以得到。

但是对于比较复杂的表达式(多个运算符或多个数据类型),必须在写代码的时候清楚地理解其语言含义,否则就会出现词不达意的效果——编译器会按另外的解释执行这些代码。

3.3.1 流行的谬误:优先级决定运算次序

所谓运算符的优先级(Precedence of Operators),一种简单易懂但却错误的说法是指在有多个运算符的表达式中运算次序的规则(8),优先级高的运算符的运算被首先执行,优先级低的运算之后被执行。例如下面的表达式:

3.3 谁是谁的谁 - 图1

实际和数学里的运算次序规则是一样的,所以C语言中“优先级”无非相当于程序中“先乘除后加减”规则,不过在程序中运算远不止乘除加减这4种而已。

然而这种说法尽管通俗却是错误的。事实上这种说法在有些情况下会得到自相矛盾的结论(如对于后缀++运算),所以有必要对前面的错误的说法做必要的修正。我知道这会有些风险,但是程序设计语言是容不得模糊的,C语言更是如此。

简单地说,除了初等表达式,表达式都是由运算符和操作数构成的,优先级规定的并不是运算的次序而是确定运算符的运算对象(也就是操作数)的先后次序。这和运算次序有些相近,但绝对是两回事情。

回过头来重新审视这个表达式:

3.3 谁是谁的谁 - 图2

它真正表达的意思是:由于运算的优先级高于+,所以的运算对象是2和3,而2*3的值与1共同构成了+的运算的对象。因而上面这个表达式等价于

3.3 谁是谁的谁 - 图3

再比如:

3.3 谁是谁的谁 - 图4

在这个表达式中有两个运算=和+,由于+的优先级高于=,因此表达式相当于:

3.3 谁是谁的谁 - 图5

计算机将把(3+j)这个表达式的值赋给变量i。

由此可见,优先级的作用是和在表达式中加分隔符()是一样的,可以确定或明确究竟谁是谁的运算对象。在对优先级十分清楚的前提下,表达式可以写得十分简明。在对优先级感到模糊的情况下,可以利用分隔符()明确地向编译器运算符的运算对象是谁——这丝毫不会影响程序的执行效率。尽管利用()的做法使代码不那么“简明”而是有些“繁明”,但“明”总比希里糊涂、似是而非的写法要好得多。而且,对于代码来说,任何情况下,“明”都应该是第一位的,“简”是第二位的。

在有些情况下,()是必须的。比如:

3.3 谁是谁的谁 - 图6

由于+的优先级低于,只能通过添加()来告诉编译器,1和2是运算符+的操作数,而初等表达式(1+2)的值与初等表达式3的值是的运算对象。这同时说明了另一件事,那就是()会改变运算次序的说法也是错误的。

练习

分析下面表达式中各个运算符的运算对象是哪个表达式,利用()写出其等价的表达式:f=-a/c+b*d%-e

3.3.2 “左结合性”是运算对象先与左面的运算符相结合吗

对优先级的规定,解决了确定表达式中存在多个不同优先级别的运算符情况下的运算对象从属问题。运算符的结合性(Associativity of Operators)解决的问题是表达式中存在多个优先级相同运算符情况下各个运算符的运算对象的从属问题。比如:

3.3 谁是谁的谁 - 图7

在这个表达式中存在3个赋值运算,由于赋值运算的结合性是从右至左,因此最右面的“=”运算符的运算对象是c和0,这个运算将求出了子表达式c=0的值,副效应是导致变量c被赋值为0,因而c=0的值就是0。所以上面表达式等价于:

3.3 谁是谁的谁 - 图8

这时,可以很清楚地看出第二个“=”的运算对象分别是b和(c=0),因而上面表达式又等价于:

3.3 谁是谁的谁 - 图9

这样就不难看出最后b、a都将被赋值为0,且整个表达式的值最后也为0。

练习

下面三个表达式的值各为多少?

3.3 谁是谁的谁 - 图10

3.3.3 运算符、表达式小结

1.C是一种“动词语言”

如表3-1所示,给出了目前遇到过的运算符的优先级和结合性以及所要求的运算对象。

表3-1 部分运算符的优先级和结合性

3.3 谁是谁的谁 - 图11

1 一种类型的名字不是数据,类型的名字可以作为运算符的一部分参与运算,但必须用()括起来。

3.3 谁是谁的谁 - 图12

表中所谓的算术类型是指前面所讲过的各种整数类型和实浮点类型(9)

表中所罗列的只是部分运算符,因此表示优先级的数字不是连续的。表示优先级的数字越大,优先级别越高。

在表中还可以看到,有两对完全相同的运算符+、-,这是典型的C语言中的一词多义的现象。对于这种运算符,具体的含义必须根据代码的上下文才能确定。

在C语言中,分析一个表达式的含义必须要寻着运算符这条主线,操作数则居于次要的运算符的从属地位。这隐约地暗示了C是一种以“动词”为主导的语言。

对表达式的分析和解读,并不是在确定运算对象的运算符,而是在确定运算符的运算对象。

2.优先级不是先乘除后加减

不得不再次强调的一点是,C语言的优先级和结合性规定的不是运算次序,只是规定了运算符的选择运算对象的次序。

根据C语言的规则并不唯一地决定表达式的运算次序,编译程序可以根据需要对于运算次序进行改变(根据交换律和结合律)。即使在存在括号的情况下,也是容许的。臆测编译器安排的运算次序往往是荒谬的,也是无益的。这一点需要特别注意。那种以为可以根据优先级和结合性就可以确定运算次序的念头往往是徒劳的。事实上,C语言在运算次序问题上只做了很有限的几条宽松的规定。因此绝对不要写那种结果依赖于运算次序的表达式。

3.掌握表达式

写C代码而不真正掌握表达式就如同写作文不会使用词组一样蹩脚。

表达式部分需要掌握的基本编程技能包括下几个方面。

■ 正确地识别、分析表达式的含义。

■ 正确地书写表达式,尽量简洁,但应该以清晰和具备确定性为前提。

■ 书写易读、美观的表达式。

最后一条似乎是个有些过分的要求,很难做到,也没有客观的标准。然而这是一个优秀的程序员必备的职业素养。

能够提供的建议是,在书写代码时应该注意到,作为一个完整的单词,各种运算符原则上是不可分割的。但是在单词之间(运算符与运算符之间以及运算符与操作数之间)适当加上空格通常是一个良好的编程习惯。如果表达式太长可以分成几行来写,只要不造成单词的割裂(10),一般不会影响编译器阅读理解表达式。那种把几条语句挤在一行来写的代码往往给人一种小家子气的感觉。

下面的程序代码,虽然丑陋,然而却清楚地表明了代码中单词的位置可以灵活到什么程度。你所需要做的是,利用这种灵活性,把代码写的易读、美观、优雅。

程序代码3-3

3.3 谁是谁的谁 - 图13

练习

修改上面代码,使其具备较好的可读性。