副作用与顺序点

先来看看这两个概念是如何定义的:

副作用(side effect)是对数据对象或文件的修改。

顺序点(sequence point)是程序执行中的一点,在该点处,所有的副作用都在进入下一步前被计算。在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。

仅有上面这一条原则还不够,例如a[i++] = i;的变量i只改变了一次,但结果仍是Undefined,因为等号左边改i的值,等号右边读i的值,到底是先改还是先读?这个读写顺序是不确定的。但为什么i = i + 1;就没有歧义呢?虽然也是等号左边改i的值,等号右边读i的值,但你不读出i的值就没法计算i + 1,那拿什么去改i的值呢?所以这个读写顺序是确定的。写表达式应遵循的原则二:如果在两个Sequence Point之间既要读一个变量的值又要改它的值,只有在读写顺序确定的情况下才可以这么写。

那么,C/C++中,哪些地方是顺序点呢?

  • 每行语句的分号;
  • 函数调用中,在所有参数求值完成后,函数体开始执行前有一个顺序点,而参数间的逗号处则没有顺序点。所以f(++i, ++i)的行为也是未定义的;
  • 逻辑运算符&&、||还有条件运算符?:的第一个参数末尾处有一个顺序点(即“||”处,“&&”处和“?”处);
  • 逗号表达式中每一个逗号处有一个顺序点。因此x = (++i, ++i)是有确定行为的;
  • 在每一个完整的变量声明处有一个顺序点,例如int i, j;中逗号和分号处分别有一个顺序点;
  • for循环控制条件中的两个分号处各有一个顺序点。

标准对“顺序点”及其语义的定义,是为了严谨地定义C/C++的表达式和求值过程,并不是为了让程序员通过对顺序点的掌握,(过分地)利用表达式求值的副作用。实际工作中,我们可以通过使用括号和临时变量来避免未定义的行为的。避开“顺序点”这样容易出错,也极大地降低代码可读性的“边缘概念”。

 

参考资料:《C Primer Plus》,http://stdcpp.cn/html/1/2/0605/149.htmhttp://www.cnblogs.com/smwikipedia/archive/2008/06/25/1229984.html

              http://learn.akae.cn/media/ch16s03.htmlhttp://www.dutor.net/index.php/2010/09/side-effect-and-sequence-point/

posted @ 2010-10-06 23:28  jeff_nie  阅读(463)  评论(0编辑  收藏  举报