关系讲解:Sequence Point时序点、副作用、完整表达式

2018-03-25  本文已影响0人  reaganax

出现这篇文章的初衷,是因为对教学内容的不解,在C Primer中也没有找到很好的解答,遂将自己找到的资料在这里做一个汇总,也聊表心意的分享给大家,觉得前面赘述的请从分割线处开始阅读。

从一个问题开始

对于下面这个程序的运行结果是什么?
int x = 1,y;
y = x++ + x++;
相信对于部分C语言初学者会给出y=2或者y=3这些不同的结果,那么问题就产生了到底此时的y的值到底是什么呢?
或许你还会疑惑:

那么首先我将抛开问题从基本的几个概念讲起

这部分的内容参考了 Eternity的文章内容,如果对于我的讲解有疑惑也可以看看这篇文章。

主要要讲解的有:时序点(Sequence Point)、完整表达式、副作用、以及其与运算优先级的关系


从这里可以得出一个结论:所有的副作用都必须在时序点之前完成。(注意这里之前的意思并不是说刚好在这个时序点之前或者说到达时序点所有的副作用才开始全部一起产生)

前面看了在C Primer Plus里面对于副作用的解释,在这里我们再次介绍一下Side-effect
基本上只要是会对变量做改变的都算作Side-effect即副作用。譬如a=b这种会改变a的状态的行为就是“=”等号这个运算符的副作用,“i++”的副作用就是会把i的值做“+1”。同样的如果一个函数foo(i)会改变i,那么这个改变行为就是foo()这个函数式子的副作用,当然side-effect包括的不仅仅是对变量数值的改变。
在C语言执行标准(版本未知)中有如下定义:

Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.

译文:
访问易变对象,修改对象或文件,或者调用包含这些操作的函数都是副作用,它们都会改变执行环境的状态。计算表达式也会引起副作用。执行序列中某些特定的点被称为序列点。在序列点上,该点之前所有运算的副作用都应该结束,并且后继运算的副作用还没发生。

对于Side-effect的具体讲解会在后面的补充文章当中讲到,这里仅仅讲解对变量改变这一类型,也是为了能够简单的阐述清楚文章的主要内容——对于概念关系的理解。

那么讲到平行的程序最常见的BUG又是什么呢?当然是出现最开始讨论的问题的情况——出现了竞争。
回到平行程序设计的角度来看,开辟了一条“通道”去把一个变数+1,随后又遇到另一个函数在Sequence Point时序点之前,开辟了另一条通道去把这个变量+1,就像前面的示意图一样,那么最终的结果会是什么呢?你可能会认为结果就是加两次的结果但是,在C语言中这个答案真的不是这样的,或者说天晓得会有什么样的答案。

注:对于这里的答案你或许会有疑惑,笔者也同样对此抱有不解的态度,所以在后期会对这里的结果作出解释。

所以从这个例子我们尚且可以归纳出一条程序的撰写准则:不能在时序点前改变一个变量的数值两次

接下来看一个与文章开头比较类似的例子:
i++ + ++i;
首先我先告诉你在C语言标准当中“+”不是一个时序点 并且“;”分号是一个时序点标志(具体对于时序点的讲解也在后面一点会讲到)。因为“+”不是一个时序点所以这个程序会开辟出三个通道。(具体哪几个通道??待更新)
所以对于这个程序最后的结果会是多少也是不确定的。
或许此处也会有一个疑问都是开辟一个自增的变脸为什么i++会在之后自增,而++i则不会呢(文章更新后补充)

接下来可能会有人问:那i=i++呢?
首先明确一点就是:“=”等号不是时序点。其次以平行程序的角度来看前面i++ + ++i有三条通道,i=i++其实有两条通道,“=”等号在主通道里对i的值更新,而++则是在另一条通道里面对i的值做更新,所以当然是不行的,有两条通道对i的值做更新。

那么哪些是时序点呢?在C99标准中的Annex C确实有明确的整理出来:

The following are the sequence points described in 5.1.2.3:
——The call to a function, after the arguments have been evaluated (6.5.2.2).
——The end of the first operand of the following operators: logical AND && (6.5.13); logical OR || (6.5.14); conditional ? (6.5.15); comma , (6.5.17).
——The end of a full declarator: declarators (6.7.5);
The end of a full expression: an initializer (6.7.8); the expression in an expression statement (6.8.3); the controlling expression of a selection statement (if or switch) (6.8.4); the controlling expression of a while or do statement (6.8.5); each of the expressions of a for statement (6.8.5.3); the expression in a return statement (6.8.6.4).
——Immediately before a library function returns (7.1.4).
——After the actions associated with each formatted input/output function conversion specifier (7.19.6, 7.24.2).
——Immediately before and immediately after each call to a comparison function, and also between any call to a comparison function and any movement of the objects passed as arguments to that call (7.20.5).

第一点说的是,在foo(i++)在控制程序真正跳进foo( )内部之前,i++的Side-effect必须完成。
特别需要注意个是,并不意味着传进去foo( )的会是“i+1”的的结果。
要知道传进去的是i++这条运算式的运算结果而不是,受附加效果影响后的值,因此传进去的还是i的原值。

现在看一个更具体的例子:首先假设i的初始值为1,那么写foo(i++,i++,i++)会发生什么事呢?
首先说明foo( )里面的“,”逗号只是用于间隔函数参数,并不是上面第二条当中所说的“comma”“,”逗号运算符。所以第二条规则在此处是不适用的。
根据上一段,很容易得出结果是foo(1,1,1),但是在编译器中可能会得到foo(3,2,1)。
这里肯定会有人质疑说,不是前面讲过“i++”运算传入foo( )中的是函数的原始值?这的确是没错的但是请注意一点:
正如前面所说所有的副作用(Side-effect)必须在时序点之前完成这句话,并不代表着刚好在时序点(Sequence Point)之前才完成。

副作用(Side-efffect)完成时有一个时间范围的,也就是说从副作用开始到时序点结束这段时间内,都有可能完成这个副作用。
所以从概念上来说,上一个程序其实开辟了三条通道去更新i的值,而交汇点是在所有的函数参数全部求值完后,到进入foo( )中的这一瞬间。此外标准并没有规定函数参数的求值顺序,所有哪条通道先开启是个未知数。所以foo(3,2,1)只是刚好编译器倒着顺序求值,而更新i的时序点刚好落在进入foo( )之前了而已。

上一篇下一篇

猜你喜欢

热点阅读