3.6 求值
运算符的优先级仅仅是“一个等级,等级高的运算符优先与旁边的操作数结合”,而并不是指优先计算。运算符的结合性也一样,仅仅是在运算符的优先级相同时,指示哪一个才有权先选择操作数,和谁先计算也没有关系。对于这一点,不单单是很多C语言的初学者搞错,即使是那些已经学过了C语言的人也经常在这里栽跟头。
我想我们需要一个活生生的例子,尽管截至目前我们已经接触过很多表达式,它们都可以作为例子,但都不够典型。那么,我们先从一个最典型的例子入手。假定a、b和c都是int类型的变量,且我们要计算以下表达式的值:
++ a + b * c
这里出现了一个我们还没见过的运算符“*”,它的作用是计算两个数的乘积,和我们平时做乘法是一样的。运算符*需要一左一右两个操作数,它的优先级比运算符+要高,但比运算符++要低,这三个运算符的优先级由高到低分别是++、*和+。现在,你能说是先计算++,再计算*,最后计算+吗?或者说,是先计算出++ a的值,再计算b * c的值,最后计算前两者的和吗?
答案是不能。
优先级高的运算符有权先选自己的操作数,所以b和c是运算符*的操作数;而a则是运算符++的操作数;运算符+的操作数只能是++ a的结果和b * c的结果。即,这个表达式等价于(++ a)+(b * c)。
因此,要得到运算符+的结果,必须先计算其操作数++ a和b * c,但可以先计算++a再计算b * c,也可以先计算b * c再计算++ a。进一步地,要计算运算符*的结果,必须先计算其操作数b和c,也就是进行左值转换。但是,可以先计算b再计算c,也可以先计算c再计算b。
当然,这里还存在着其他可能的计算顺序。如果将运算符++的值计算记为V++,将运算符*的值计算记为V*,将运算符+的值计算记为V+,将b和c的值计算分别记为Vb和Vc,则表达式++ a + b * c的计算过程可以有多种不同的顺序,以下列举了其中的一部分(假定变量a、b和c的当前值分别为0、1和2,括号中的数值为计算出来的结果)。
Vb(1)→ Vc(2)→ V*(2)→ V++(1)→ V+(3)
Vc(2)→ Vb(1)→ V*(2)→ V++(1)→ V+(3)
V++(1)→ Vb(1)→ Vc(2)→ V*(2)→ V+(3)
V++(1)→ Vc(2)→ Vb(1)→ V*(2)→ V+(3)
Vb(1)→ V++(1)→ Vc(2)→ V*(2)→ V+(3)
Vc(2)→ V++(1)→ Vb(1)→ V*(2)→ V+(3)
Vb(1)→ Vc(2)→ V++(1)→ V*(2)→ V+(3)
Vc(2)→ Vb(1)→ V++(1)→ V*(2)→ V+(3)
显然,运算符的优先级和它是否被优先计算无关,子表达式的计算顺序通常也没有什么规律可言,但并不影响最终的结果。当然,正如我们曾经强调过的,在这看似没有规律和顺序的计算过程中,最基本的原则是先计算运算符的操作数,再计算运算符本身的值。
表达式++ a + b * c不仅要计算出一个值,它还有副作用,因为它的子表达式++ a是有副作用的表达式。但是,这个副作用什么时候发起?
一旦将副作用也考虑进来,事情就变得更加复杂了。我们不单要考虑值计算的顺序,还要关心副作用的发起时间,这就需要一个新的术语“求值”来涵盖这两个层面。我们知道,表达式可以指示变量或者函数,也可以计算一个值,还可能发起一个副作用,而“求值一个表达式”则通常包括值计算和发起一个副作用。当然,有些表达式没有副作用,那么它的求值仅仅包含值计算。
所以,我们现在可以这样问:表达式++ a + b * c求值的时候,子表达式的值计算和副作用按什么顺序进行?
对于这个问题,C语言的规定是很明确的:除非另有指定,在表达式求值的时候,子表达式的值计算和副作用之间没有明确的顺序,或者说是无序的。
之所以这样规定,是希望把决定权交给翻译软件,由它们在翻译程序的时候自主决定,这样可以生成更加紧凑和高效的机器指令。
这里的“另有指定”,是针对一小部分特殊的表达式来说的,比如,对于后缀++运算符来说,它的值计算发生在(修改其操作数所代表的变量的存储值的)副作用之前;对于简单赋值和复合赋值运算符来说,(修改其左操作数所代表的变量的存储值的)副作用发生在其左右操作数的值计算之后。
因此,假定把子表达式++ a的副作用记为S++,则表达式++ a + b * c求值时,其子表达式的值计算和副作用可以有更多的顺序,以下列举了其中的一小部分:
Vb(1)→ Vc(2)→ V*(2)→ V++(1)→ S++(1)→ V+(3)
Vc(2)→ Vb(1)→ V*(2)→ V++(1)→ V+(3)→ S++(1)
V++(1)→ Vb(1)→ Vc(2)→ V*(2)→ S++(1)→ V+(3)
V++(1)→ Vc(2)→ Vb(1)→ V*(2)→ V+(3)→ S++(1)
Vb(1)→ V++(1)→ Vc(2)→ V*(2)→ S++(1)→ V+(3)
Vc(2)→ V++(1)→ Vb(1)→ S++(1)→ V*(2)→ V+(3)
Vb(1)→ Vc(2)→ V++(1)→ V*(2)→ S++(1)→ V+(3)
Vc(2)→ Vb(1)→ V++(1)→ V*(2)→ V+(3)→ S++(1)
显然,修改变量a的存储值的副作用可以发生在整个表达式求值期间的任何时候。再以我们前面所讨论的while语句为例:
while(n <= r)sum = sum + n ++;
整个表达式sum = sum + n ++的值也是运算符=的值,记为V=;修改变量sum存储值的副作用也是运算符=的副作用,记为S=;表达式sum + n ++的值也是运算符+的值,记为V+;表达式n ++的值也是后缀递增运算符的值,记为V++;表达式n ++的副作用也是后缀递增运算符的副作用,记为S++;中间那个表达式sum的值是Vsum。那么,这整个表达式的求值顺序可以是:
V++→ S++→ Vsum→ V+→ V=→ S=
也可以是:
V++→ Vsum→ V+→ V=→ S++→ S=
还可以是:
Vsum→ V++→ V+→ V=→ S=→ S++
当然,还可以有其他更多的求值顺序,只要它们符合前面的几个约束条件(运算符操作数的值计算要先于运算符的值计算;后缀递增运算符++和赋值运算符特有的求值顺序)。但是无论求值顺序有多少种,都不影响最终结果的正确性。
练习3.5
假定变量n和sum当前的存储值分别为1和0,请在表达式sum = sum + n ++里代入这两个值以验证上述几种求值顺序不影响最终的结果。