3.7.1 全表达式和序列点
在解决逗号表达式的求值顺序问题之前,我们需要先来了解另外两个概念:全表达式和序列点。总体上,如果一个表达式在形式上是独立的,不是其他表达式的组成部分,也不是一个声明符的组成部分,那么它就是一个全表达式。
声明符是变量声明或者函数声明的一部分,用来描述被声明的实体。最简单的声明符是一个标识符,例如在以下声明中,标识符m和n就是声明符:
unsigned long int m, n = 0;
有些声明符相对复杂,在下面的函数声明中,声明符是func(int x)。显然,这个声明符不单纯是标识符,还包括标识符后面的参数列表。
int func(int x);
取决于声明的是什么东西(类型),声明符可能会多种多样,而且有些声明符里会包含表达式,你很快就会接触到这样的声明符。
继续来讨论全表达式,为了增加感性认识,我们以下面的程序代码为例,来看看哪些是全表达式:
unsigned long long int cusum(unsigned long long int r) { unsigned long long int n = 1, sum = 0; while(n <= r)sum += n ++; return sum; }
首先,表达式语句由表达式和分号“;”组成,表达式语句中的表达式是全表达式。也就是说,将语句
sum += n ++;
末尾的分号“;”去掉之后,剩下的部分就是全表达式。
其次,while语句的控制表达式也是全表达式,所以n<= r是全表达式。实际上不单单是while语句,但凡是需要控制表达式的语句,其控制表达式往往都是全表达式。
第三,如果return语句是由关键字“return”和表达式组成的,则该表达式也是全表达式。所以return语句中的表达式sum是全表达式。
最后,很多初始化器都是全表达式。在这里,用于初始化变量n和sum的表达式0、1是全表达式。
全表达式还有很多,但是凭我们现在所掌握的C语言知识还无法全部列举,所以要留到本书的后面再一一介绍。
另一个概念“序列点”则与表达式的求值有关。给定任意两个表达式A和B,如果A的值计算和副作用发生在B的值计算和副作用之前,则我们说在A和B的求值之间存在一个序列点。显然,序列点是一个求值的界线,前一个表达式的值计算和副作用已经完成,而后一个表达式的值计算和副作用还没有开始。
C语言规定,在一个全表达式的求值和下一个全表达式的求值之间存在一个序列点。如图3-1所示,在while语句内有三个全表达式,所以也存在三个序列点。
图3-1 序列点示意图
显然,序列点可以保证程序的行为精确可控,执行的结果可以预测。例如,要是while语句开始下一轮循环时,全表达式n = n + 1的副作用已经发起并完成,则控制表达式n<= r的求值可以用到变量n的新值;要是没有序列点的存在,你无法保证n<= r求值的时候,表达式n = n + 1的副作用是否已经发起并完成。在这种情况下,也就无法保证表达式n<= r的求值是否能用上变量n的新值。
讲完了全表达式和序列点,让我们继续逗号表达式的话题。逗号运算符有一左一右两个操作数,C语言规定,在其左操作数的求值和右操作数的求值之间有一个序列点。
这就是说,在对表达式sum += n, n ++求值时,是先求值左操作数sum += n,当它的值计算和副作用都完成后,才开始求值右操作数n ++。因此,绝对不会发生我们在前面所担心的事情:先求值n ++,或者混乱交叉求值。
逗号运算符的值是其右面那个操作数的值,左操作数的值被丢弃。所以,表达式5, 6的值是6;表达式sum += n, n ++的值是其子表达式n ++的值。
这么说来,逗号运算符的左操作数似乎没有什么存在的意义和价值。实际上,设计逗号表达式的目的是希望逗号运算符的左操作数有副作用。这样,在实际求值的时候,左操作数的意义体现在它的副作用上,右操作数则提供了整个逗号表达式的值。
从这个意义上来说,表达式5,6虽然是合法的逗号表达式,但没有什么用处;表达式sum += n, n ++的左操作数sum += n是有副作用的表达式,我们只关注它的副作用;右操作数是n ++,它既有副作用,又提供了整个逗号表达式的值。
练习3.7
1.在以下代码片段中,逗号表达式为( ),它的值为( )。如果要把该逗号表达式的值赋给变量m,应该怎么修改第二行?(注意,我们说过,在所有运算符里,逗号运算符的优先级最低)。
long int m, x, y, z = 0; y = z, x = ++ y;
2.对于逗号运算符,如果左右操作数求值之间不存在序列点,那么,请用不同的求值过程推演一下,看一看每当表达式sum += n, n ++求值完成后,变量sum、n和整个表达式的值是否不受求值顺序的影响。