分类: C/C++
2011-05-11 16:35:13
from:
什么是sequence point?
表达式的计算分为两种,
一种是有副作用的计算,如:(++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++的表达式和求值过程,并不是为了让程序员通过对顺序点的掌握,(过分地)利用表达式求值的副作用。实际工作中,我们完全可以通过引入中间变量,避开“顺序点”这样容易出错,也极大地降低代码可读性的“边缘概念”。
当你写出违反顺序点定义的表达式的时候,子表达式求值的顺序会影响表达式的结果,这种情况正是标准希望通过“顺序点”的定义来避免的情况。
如果你不违反顺序点的定义,子表达式求值的顺序则不会影响整个表达式的结果,但是一个符合顺序点定义,计算顺序不影响其结果的表达式,仍然可以有副作用。所谓副作用,是相对传统意义上的“表达式”来说的。
例如,传统表达式的计算过程,运算符不会令参与计算的变量本身的值发生改变;而C/C++语言的表达式中由于++,
--等运算符的介入,表达式求值可能导致参与计算的变量本身的值发生改变。这就是一种可能的副作用。
如果单从表达式本身的计算结果看,这两个表达式的副作用当然不会影响整个表达式的值;在C语言中对表达式 (++i) + (++j) 求值,会使i的值加一,j的值加一。你不对这两件事发生的顺序作假设,而只关心整个表达式的最终结果。从这个意义上说,你写的这个表达式符合顺序点的定义,因而它的计算结果,按照标准定义,是没有歧义的。整个表达式的“计算结果没有歧义”不等于说它“没有副作用”,i和j的值发生改变正是这个表达式求值的副作用。
C语言中,只包含一个表达式的语句,如
x = (i++) * 2;
称为“表达式语句”。表达式语句结尾的";"是C标准定义的顺序点之一,但这不等同于说所有的";"都是顺序点,也不是说顺序点只有这一种。
下面就是标准中定义的顺序点:
函数调用时,实参表内全部参数求值结束,函数的第一条指令执行之前(注意参数分隔符“,”不是顺序点);
&&操作符的左操作数结尾处;
||操作符的左操作数结尾处;
?:操作符的第一个操作数的结尾处;
逗号运算符;
表达式求值的结束点,具体包括下列几类:
自动对象的初值计算结束处;
表达式语句末尾的分号处;
do/while/if/switch/for语句的控制条件的右括号处;
for语句控制条件中的两个分号处;
return语句返回值计算结束(末尾的分号)处。
定义顺序点是为了尽量消除编译器解释表达式时的歧义,如果顺序点还是不能解决某些歧义,那么标准允许编译器的实现自由选择解释方式。理解顺序点还是要从定义它的目的来下手。
再举一个例子:
y = x++, x+1;
已知这个语句执行前x=2,问y的值是多少?
逗号运算符是顺序点。那么该表达式的值就是确定的,是4,因为按照顺序点的定义,在对x+1求值前,顺序点","前的表达式——x++求值的全部副作用都已发生完毕,计算x+1时x=3。这个例子中顺序点成功地消除了歧义。
注意这个歧义是怎样消除的。因为中间的顺序点使“相邻顺序点间对象的值只更改一次”的条件得到满足。
y = (x++) * (x++), 执行前x=2, y=?
答案是,因为这个表达式本身不包含顺序点,顺序点未能消除歧义,编译器生成的代码使y取4, 6(以及更多的一些可能值)都是符合标准定义的,程序员自己应为这个不符合顺序点定义的表达式造成的后果负责。
我对我自己的表达能力欠佳表示抱歉,但我的确不准备对这个问题再做更多的解释。我愿意引用《Expert C Programming》中的一段话,来给自己找一个下台阶:
However, the problem with standards manuals is that they only make sense if you
already know what they mean. If people write them in English, the more precise
they try to be, the longer, duller and more obscure they become. If they write
them using mathematical notation to define the language, the manuals become
inaccessible to too many people.
自然语言本身的不精确,往往容易造成越解释越不清楚的现象,而精确的数学语言,又已经超过包括我在内的大多数人的理解和应用能力。
谢谢the best提供的这个机会来检验对知识点的理解程度;也谢谢斑竹对这个讨论的支持。
在C++里面我们写一个表达式,我们认为只有一个目的:
i=j++;//目的是为了给i赋值
func(i++);//目的是为了调用函数
所以j被加1和i被加1这样的结果,我们认为是“副作用”。这就好比你吃药杀葡萄球菌,但是把大肠杆菌也杀了,这就是副作用。
我们再看一个表达式
j=(i++)+(i++);
i++这个式子的意思大家都知道,就是取i过去的值,然后把i加1。那么编译器开始分析这个式子了。
它可以这样做:取i的值,然后再取i的值(加1的事情当然要做,可是没说什么时候),然后把这两个i加起来,再给i做两次加1操作
也可以这样做:取i的值,然后给i加1,然后再取i的值,...
总之,不同的解释方法,结果不同,你去写这样的表达式,就是玩火。