详述C疑难杂症_序列点

来源:互联网 发布:淘宝网店实战宝典下载 编辑:程序博客网 时间:2024/06/05 19:53
优先级 结合性与序列点 转载自:http://blog.chinaunix.net/uid-24745601-id-178300.html

序列点是程序中这样的一些点:通俗地说,执行至此,之前的语句都已经彻底执行干净执行完了,之后的语句还完全没开始执行;更常见、更严谨但略晦涩的说法是,之前的语句对现场环境的改变已经全部完成,之后的语句对现场环境的改变还没有开始。啥是现场环境呢?就是程序执行到某一点的那个状态,包括变量的内容、文件的内容等。

i=i+1的过程相当:
  temp=i+1; i=temp;
i++ 的过程相当:
  temp=i; i=temp+1; return temp;
++i 的过程最简单:
  i 增1然后return i 的值,没有给任何temp变量赋值


int i = 3;

 i = i++;

 cout << i;

标准规定,两个序列点之间,程序执行的顺序可以是任意的。没错,正如你猜的那样,C++标准规定一个完整的表达式结束之后有一个序列点,而例子中i = i++是位于两个序列点之间的。编译器可以先算完i++,再写结果给i,也可以先将i = i,再令i++.按前面的方法算,i先自增变为4,然后i++返回3,于是i被赋值为3;按后一种方法算,i先被赋值为3,随后自增变成4.标准说了,这两种处理方法,编译器你爱选那种就选哪种,随便。如果谁写的程序像这样依赖执行的顺序,让他自己哭去!

如何避免由序列点造成的这种未定义行为,有一句经典但有点晦涩的编程规则:“在相邻的两个序列点之间,一个对象只允许被修改一次,而且如果一个对象被修改则在这两个序列点之间只能为了确定该对象的新值而读一次”。其实明白了序列点具体是怎么回事,这个规则应该就很容易明白了。由于序列点之间程序执行顺序不确定,一个对象被修改多次的话最后留下的是哪次的结果就不确定。另外如果一个对象同时存在读取和修改,只有根据读取的结果来修改才是合法的,否则就会出现是先改完再读还是先读完再改的混乱。


一个表达式可以分两部分来看:
副作用 - side effect, 我觉得效果这个词比较形象一点。 意思是一个表达式带来的影响
结果 - result , 就是表达试的值
比如i++, 副作用是i+1,结果是i本身。

然后是三个名词:
优先级 - precedence
结合性 - associativity
序列点 - sequence point


很多人错误的理解了优先级和结合性的意思,认为它们决定了一个表达式中各子表达式的求值顺序,这是错误理解文章开头所提问题的根源。  而实际上:

1.  优先级和结合性是的唯一作用是决定哪些操作数是属于那些操作符。

2.  优先级和结合性不能表明那些表达式先求值那些后求值。 

3.  序列点就是程序执行过程中的特定点,子表达式的求值顺序只被序列点决定。 

4.  C语言只保证,到一个序列点的时候,这个序列点前面的所有side effect都已经完成,而且这个序列点后面的所有side effect都还没有发生。

5.  上一个序列点后到这一个序列点之间,各子表达式的求值顺序未定义。

6.  序列点包括:

     a> 函数调用(在参数处理完成,实际进入函数之前)

     b> 在操作符逻辑与(&&), 逻辑或(||),条件(?),逗号(, 函数参数列表里不算)的第一个操作数之后,主要是为了发挥短路的作用。

     c> 一个完整的表达式后,比如分号后面或者if里面的控制表达式等。

     d>完整声明之后 。

     e>进入函数时与退出函数时。

7. 对于序列点,一个重要的原则是: 在上一个序列点后到这一个序列点之间最多改变一个对象的值一次。


例如表达式:

a=i + i++ * i++ - b;


由于++的优先级最高,*次之,+和-的优先级一样,但结合性是从左到右,所以这个表达式会被解析成:


a=(+ ((i++) * (i++))) - b;


这就是优先级和结合性的唯一作用,只是表明操作数应该往哪个操作符上靠。 至于其中的子表达式比如两个i++什么时候发生,什么顺序发生,都是未定义的,不确定的。

而像下面这个:

i++ && i++ && i++

其执行顺序就是确定的,因为&&是序列点,每到一个&&, 就可以保证其前面所有的side effect已经发生。

详述序列点和副作用  转载自:http://duanple.blog.163.com/blog/static/70971767200861711542394/
C 语言中,术语副作用(side effect)是指对数据对象或者文件的修改。例如,以下语句

        var = 99;

的副作用是把 var 的值修改成 99。对表达式求值也可能产生副作用,例如:

        se = 100

对这个表达式求值所产生的副作用就是 se 的值被修改成 100。

    序列点(sequence point)是指程序运行中的一个特殊的时间点,在该点之前的所有副作用已经结束,并且后续的副作用还没发生。

    C 语句结束标志——分号(;)是序列点。也就是说,C 语句中由赋值、自增或者自减等引起的副作用在分号之前必须结束。我们以后会说到一些包含序列点的运算符。任何完整表达式(full expression)运算结束的那个时间点也是序列点。所谓完整表达式,就是说这个表达式不是子表达式。而所谓的子表达式,则是指表达式中的表达式。例如:

        f = ++e % 3

这整个表达式就是一个完整表达式。这个表达式中的 ++e、3 和 ++e % 3 都是它的子表达式。

    有了序列点的概念,我们下面来分析一下一个很常见的错误:

        int x = 1, y;
        y = x++ + x++;

这里 y = x++ + x++ 是完整表达式,而 x++ 是它的子表达式。这个完整表达式运算结束的那一点是一个序列点,int x = 1, y; 中的 ; 也是一个序列点。也就是说,x++ + x++ 位于两个序列点之间。标准规定,在两个序列点之间,一个对象所保存的值最多只能被修改一次。但是我们清楚可以看到,上面这个例子中,x 的值在两个序列点之 间被修改了两次。这显然是错误的!这段代码在不同的编译器上编译可能会导致 y 的值有所不同。比较常见的结果是 y 的值最后被修改为 2 或者 3。在此,我不打算就这个问题作更深入的分析,各位只要记住这是错误的,别这么用就可以了。有兴趣的话,可以看看以下列出的相关资料。

相关资料:
  自增运算符和自减运算符
  让我耿耿于怀的一道笔试题

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.

翻译如下:

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

参考资料:C Primer 5th Edition
          The C Programming Language 2nd Edition
          C99 标准


理解C中的序列点  转载自:http://www.kunli.info/2008/05/06/c-sequence-points/

让我们来看看下面的代码:

int i=7; printf(“%d\n”, i++ * i++);

    你认为会返回什么?56?no。正确答案是返回 49。很多人会问为什么,难道不该打印出56吗?在ccfaq中有非常详尽的解释,根本原因在于c中的序列点。

     请注意,尽管后缀自加和后缀自减操作符 ++ 和 – - 在输出其旧值之后才会执行运算,但这里的“之后”常常被误解。没有任何保证确保自增或自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进行。也不能保证变量的更新会在表达式 “完成” (按照 ANSI C 的术语, 在下一个”序列点”之前) 之前的某个时刻进行。本例中, 编译器选择使用变量的旧值相乘以后再对二者进行自增运算。只有到达一个序列点之后,自增运算才能保证真正被执行。

     包含多个不确定的副作用的代码的行为总是被认为未定义。(简单而言, “多个不确定副作用”是指在同一个表达式中使用导致同一对象修改两次或修改以后又被引用的自增,自减和赋值操作符的任何组合。这是一个粗略的定义。) 甚至都不要试图探究这些东西在你的编译器中是如何实现的 (这与许多 C 教科书上的弱智练习正好相反)。正如 K&R 明智地指出”如果你不知道它们在不同的机器上如何实现, 这样的无知可能恰恰会有助于保护你”。

那么,所谓的序列点是什么意思呢?

序列点是一个时间点(在整个表达式全部计算完毕之后或在 ||、 &&、 ? : 或逗号运算符处, 或在函数调用之前), 此刻尘埃落定,所有的副作用都已确保结束。 ANSI/ISO C 标准这样描述:

在上一个和下一个序列点之间,一个对象所保存的值至多只能被表达式的计算修改一次。而且前一个值只能用于决定将要保存的值。

第二句话比较费解。它说在一个表达式中如果某个对象需要写入(修改),则在同一表达式中对该对象的访问应该只局限于直接用于计算将要写入的值。这条规则有效地限制了只有能确保在修改之前才访问变量的表达式为合法。

例如 i = i+1 合法,而 a[i] = i++ 则非法。为什么这样的代码:a[i] = i++; 不能工作?子表达式 i++ 有一个副作用 — 它会改变 i 的值 — 由于 i 在同一表达式的其它地方被引用,这会导致无定义的结果,无从判断该引用(左边的 a[i] 中)是旧值还是新值。那么,对于 a[i] = i++; 我们不知道 a[] 的哪一个分量会被改写,但 i 的确会增加 1,对吗?不一定!如果一个表达式和程序变得未定义,则它的所有方面都会变成未定义。

为什么&& 和 || 运算符可以产生序列点呢?这些运算符在此处有一个特殊的例外:如果左边的子表达式决定最终结果 (即,真对于 || 和假对于 && ) ,则右边的子表达式不会计算。因此,从左至右的计算可以确保,对逗号表达式也是如此。而且,所有这些运算符 (包括 ? : ) 都会引入一个额外的内部序列点


0 0
原创粉丝点击