设为首页 加入收藏

TOP

15.3 Side Effect与Sequence Point
2013-10-12 06:53:11 来源: 作者: 【 】 浏览:95
Tags:15.3 Side Effect Sequence Point

15.3  Side Effect与Sequence Point

如果你只想规规矩矩地编写代码,那么基本用不着看这一节。本节的内容基本上是钻牛角尖儿的,除了Short-circuit比较实用,其他写法都应该避免使用。但没办法,有时候不是你想钻牛角尖儿,而是有人逼你去钻牛角尖儿。这是我们的学员在找工作笔试时碰到的问题:

  1. int a = 0;  
  2. a = a++; 

据我了解,很多公司都出过这种笔试题。答案应该是Undefined,我甚至有些怀疑出题人是否真的知道答案。下面我来解释为什么是Undefined。

我们知道,调用一个函数可能产生Side Effect,使用某些运算符(++ -- = 复合赋值)也会产生Side Effect,如果一个表达式中隐含着多个Side Effect,究竟哪个先发生哪个后发生呢?C标准规定代码中的某些点是Sequence Point,当执行到一个Sequence Point时,在此之前的Side Effect必须全部作用完毕,在此之后的Side Effect必须一个都没发生。至于两个Sequence Point之间的多个Side Effect究竟哪个先发生哪个后发生则没有规定,编译器可以任意选择各Side Effect的作用顺序。下面详细解释各种Sequence Point。

1.调用一个函数时,在所有准备工作做完之后及函数调用开始之前是Sequence Point。比如调用foo(f(), g())时,foo、f()、g()这三个表达式哪个先求值哪个后求值是Unspecified,但是必须都求完值了才能做最后的函数调用,所以f()和g()的Side Effect按什么顺序发生不一定,但必定在这些Side Effect全部作用完之后才开始调用foo函数。

2.条件运算符 :、逗号运算符、逻辑与、逻辑或的第一个操作数求值之后是Sequence Point。我们刚讲过条件运算符和逗号运算符,条件运算符要根据表达式1的值是否为真决定下一步求表达式2还是表达式3的值,如果决定求表达式2的值,表达式3就不会被求值了,反之也一样,逗号运算符也是这样,表达式1求值结束后才继续求表达式2的值。

逻辑与和逻辑或早在第4.3节就讲了,但在初学阶段我一直回避它们的操作数求值顺序问题。这两个运算符和条件运算符类似,先求左操作数的值,然后根据这个值是否为真,决定是否求右操作数的值。比如例8.5中的以下几句:

  1. ret = scanf("%d", &man);  
  2. if (ret != 1 || man < 0 || man > 2) {  
  3.         printf("Invalid input!\n");  
  4.         return 1;  
  5. }  
  6. 其实可以写得更简单(类似于[K&R]的简洁风格):  
  7. if (scanf("%d", &man) != 1 || man < 0 || man > 2) {  
  8.         printf("Invalid input!\n");  
  9.         return 1;  

这个控制表达式的求值顺序是:先求scanf("%d", &man)! = 1的值,如果scanf调用失败,则返回值不等于1成立,逻辑或运算有一个操作数为真则整个表达式为真,这时直接执行printf语句,根本不会再去求man < 0或man > 2的值;如果scanf调用成功,则读入的数保存在变量man中,并且返回值等于1,则第一个逻辑或运算的左操作数为假,就会去求右操作数man < 0的值作为整个表达式的值,这时变量man的值正是scanf读上来的值,我们判断它是否在[0,2]区间,如果man < 0不成立,则整个表达式scanf("%d", &man) != 1 || man < 0 的值为假,也就是第二个逻辑或运算的左操作数为假,所以最后求右操作数man > 2的值作为整个表达式的值。

逻辑与运算与此类似,a && b的计算过程是:首先求表达式a的值,如果a的值是假则整个表达式的值是假,不会再去求b的值;如果a的值是真,则下一步求b的值作为整个表达式的值。所以,a && b相当于"if a then b",而a || b相当于"if not a then b"。这种特性称为Short-circuit,很多人喜欢利用Short-circuit特性简化代码。

3.在一个完整的声明末尾是Sequence Point,所谓完整的声明是指这个声明不是另外一个声明的一部分。比如声明int a[10], b[20];,在a[10]末尾是Sequence Point,在b[20]末尾也是。

4.在一个完整的表达式末尾是Sequence Point,所谓完整的表达式是指这个表达式不是另外一个表达式的一部分。所以如果有f(); g();这样两条语句,f()和g()是两个完整的表达式,则f()的Side Effect必定在g()之前发生。

5.在库函数即将返回时是Sequence Point。这条规则似乎可以包含在上一条规则里面,因为函数返回时必然会结束一个完整的表达式。而事实上很多库函数是以宏定义的形式实现的(函数式宏定义在第20.2.1节介绍),并不是真正的函数,所以才需要有这条规则。

还有两种Sequence Point和某些C标准库函数的执行过程相关,此处从略,有兴趣的读者可参考[C99]的Annex C。

现在分析一下本节开头的例子:int a = 0; a = a++;。按照运算符优先级应该先计算a++,再把表达式a++的值赋给a。已知a的初值是0,则表达式a++的值是0。现在有两个Side Effect,一个是在计算表达式a++之后应该把a改成1,另一个是把表达式a++的值0赋给等号左边的a,哪个先发生不一定,只知道在整个表达式求值结束时这两个Side Effect一定都发生了,最后a的值可能是0也可能是1,所以结果是Undefined。这行代码用不同平台的不同编译器来编译结果可能是不同的,甚至在同一平台上用同一编译器的不同版本来编译结果也可能不同。

由于两个Sequence Point之间的多个Side Effect可以按任意顺序发生,所以在写代码时要注意,在两个Sequence Point之间,同一个变量的值最多只允许改变一次。但是做到这一点还不足以保证代码的执行结果是确定的,一个变量被改变了一次,就有改变之前和改变之后两个不同的值,如果这个变量在一个表达式中出现多次,它应该代表哪个值呢?比如在a[i++] = i;中变量i只改变了一次,但结果仍是Undefined,我们分析一下:设i的初值是0,则表达式i++的值是0,有一个Side Effect是把i改成1,但这个Side Effect什么时候发生不一定,如果在这个Side Effect发生之后才取等号右边i的值,则把a[0]赋值为1,如果在这个Side Effect发生之前就取等号右边i的值,则把a[0]赋值为0。再比如i = i + 1;,它的执行结果是确定的,它会读取i的值,也会产生Side Effect改写i的值,但必须先读再改写,所以读取到的i值一定是初值而不是改写之后的值。C99的6.5节条款2规定:"Between the previous and next sequence point an object shall have its stored value modified at most once by the eva luation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored."


】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇15.2.4 sizeof运算符与typedef类.. 下一篇15.1.1 按位与、或、异或、取反运..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: