关系讲解:Sequence Point时序点、副作用、完整表达式及运算优先级

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

从一个问题开始

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

  • ++的优先级不是比=的优先级更高吗?先算++为什么又要在后面自增呢?那这样优先级的意义又在哪里呢?
  • 对于这种式子结果到底是什么?
  • 如果你尝试用不同编译器去编译,你又会发现结果有所不同?这又是为什么呢?

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

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

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


  • 第一点时序点(Sequence Point)副作用与完整表达式

    时序点或者说序列点(整篇文章选择时序点这个翻译来讲解)在C Primer plus书中是这样描述的(对于在本书中的全部讲解,我将放在文末):

    A sequence point is a point in program execution at which all side effects are evaluated before going on to the next step. In C, the semicolon in a statement marks a sequence point. That means all changes made by assignment operators, increment operators, and decrement operators in a statement must take place before a program proceeds to the next statement. Some operators that we’ll discuss in later chapters have sequence points. Also, the end of any full expression is a sequence point.

    译文:
    序列点(sequence point)是程序执行的点,在该点上,所有的副作用都在进入下一步之前发生。在 C语言中,语句中的分号标记了一个序列点。意思是,在一个语句中,赋值运算符、递增运算符和递减运算符对运算对象做的改变必须在程序执行下一条语句之前完成。后面我们要讨论的一些运算符也有序列点。另外,任何一个完整表达式的结束也是一个序列点。

    关于副作用、完整表达式会在稍后讲到
    书中对于时序点的阐述并不是十分明确,在Eternity的文章当中这样讲解道:
    首先为什么Sequence Point要叫做Sequence point
    叫Sequence Point仅仅是因为它看起来帅气吗?还是会让人有“学术感”呢?当然不是。

    Sequence的解释是:

    n. [数][计] 序列;顺序;续发事件
    vt. 按顺序排好

    那么非Sequence的意思是什么呢?
    是Parallel

    n. 平行线;对比
    vt. 使…与…平行
    adj. 平行的;类似的,相同的

    所以Sequence最恰当的翻译应该是被称作循序点

    对于副作用(side-effect)这个名词,也将引用Primer Plus里面的介绍作为引入

    Now for a little more C terminology: A side effect is the modification of a data object or file. For instance, the side effect of the statement
    states = 50;
    is to set the states variable to 50. Side effect? This looks more like the main intent! From the standpoint of C, however, the main intent is evaluating expressions. Show C the expression 4 + 6, and C evaluates it to 10. Show it the expression states = 50, and C evaluates it to 50. Evaluating that expression has the side effect of changing the states variable to 50. The Expressions and Statements
    increment and decrement operators, like the assignment operator, have side effects and are used primarily because of their side effects.
    Similarly, when you call the printf() function, the fact that it displays information is a side effect. (The value of printf(), recall, is the number of items displayed.)

    译文:
    我们再讨论一个C语言的术语副作用(side effect)。副作用是对数据对象或文件的修改。例如,语句:
    states = 50;
    它的副作用是将变量的值设置为50。副作用?这似乎更像是主要目的!但是从C语言的角度看,主要目的是对表达式求值。给出表达式4 + 6,C会对其求值得10;给出表达式states = 50,C会对其求值得50。对该表达式求值的副作用是把变量states的值改为50。跟赋值运算符一样,递增和递减运算符也有副作用,使用它们的主要目的就是使用其副作用。
    类似地,调用 printf()函数时,它显示的信息其实是副作用(printf()的返回值是待显示字符的个数)。

    “ i++”的副作用就是它的值会“偷偷的”+1,跟“=”这种马上+1的副作用不同
    换言之它是在background(背后)被+1的,所以我们可以做这样一种想象在程序执行到“i++”的时候编译器开辟了一条通道去把i的值更新成了“i+1”。
    这就是刚才所提到的非SequenceParallel
    既然是非线性即平行的执行的东西总需要一个汇合点进行汇合,这个交汇的地方就是所谓的Sequence Point,这就是平行模式回到循环执行模式的分界点。
    在这里放一个不太贴切的示意图

    Sequence Point and Side-effect.png

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

前面看了在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( )之前了而已。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容