表达式的计算分为两种,一种是有副作用的计算,如:
(++x)+y
一种是无副作用的计算,如:
x*y
有副作用的计算中,子表达式的计算顺序是重要的。例如
(++x)*(x+1)
当x=0时,如果先算++x,上式计算结果为2,如果先算x+1,上式计算结果为1。
再如,对函数g(int, int)的调用g(x, ++x), 当x=1,这个调用是g(1, 2)还是g(2, 2)?
所谓“顺序点”,和表达式的副作用紧密相关。再看这个例子:
(++i) + (++j)
这个表达式的计算,有两个副作用:
i自增1;
j自增1;
但是到底哪一个先发生?答案是:任何答案都不对。
为什么?因为标准并不定义副作用发生的顺序。标准只保证,一个表达式的全部副作用,不在达到该表达式紧邻的前一顺序点前发生,并且一定在达到该表达式紧邻的下一个顺序点之前发生完毕。
一个顺序点,被定义为程序执行过程中的这样一个点:该点前的表达式的所有副作用,在程序执行到达该点之前发生完毕;该点后的表达式的所有副作用,在程序执行到该点时尚未发生。
(++i) + (++j)这个表达式本身不包含顺序点,所以i++,j++这两个“副作用”到底谁先发生,根据标准,是未定义的。如果给这个表达式加上顺序点,如:
;(++i) + (++j);
标准只保证,这两个副作用在整个表达式求值完成前(即到达后面的顺序点”;”前)都会发生,并且不会在上一个语句执行完毕之前发生。
标准还规定,两个相邻顺序点之间,对某一表达式求值,最多只能造成任一特定对象的值被更改一次。如果表达式求值过程会更改某对象的值,那么要求更改前的值被读取的唯一目的,只能是用来确定要存入的新值。
例如下面的表达式,按照标准规定,执行结果是未定义的:
(i++)+(i++)
这个表达式本身不包含任何顺序点,但是对这个表达式求值,按照运算符定义,将更改i两次,违反了“一次更改”的要求。
再看下面的表达式,按照标准规定,执行结果也是未定义的:
x
=i++
这个表达式本身不包含任何顺序点,虽然i的值只更改了一次,但是x这个左值中,i被读取,用于确定数组中被修改的元素的下标。这次对i求值和i++肯定位于同一对顺序点之间,该表达式求值过程更改了i的值,x中读取i却不是为了确定i的新值,这违反了“读取只能用于确定新值”规定。
任何对相邻顺序点间表达式求值的多个副作用发生的顺序进行假设,或者违反上述“一次更改、读取仅用于确定新值”规定的代码,其执行结果都是未定义的。这里所说的“未定义”,通常比“不可移植”更严重,可以认为是“错误”的同意词。
通常我们认为,标准对“顺序点”及其语义的定义,是为了严谨地定义C/C++的表达式和求值过程,并不是为了让程序员通过对顺序点的掌握,(过分 地)利用表达式求值的副作用。实际工作中,我们完全可以通过引入中间变量,避开“顺序点”这样容易出错,也极大地降低代码可读性的“边缘概念”。