[C]副作用和序列点

概述

副作用

《C语言核心技术》对副作用的描述:

表达式内包含了一串的常量、标识符、运算符(指示的运算方式)。表达式的目的可以是获得结果值,或者得到运算的副作用(side effect),或者两者兼备。

获得结果值是比较容易理解的一句话术,比如下列这些表达式:

x + 1;
x + y;
d(e);//假设e是非指针变量
8 * 9;

它们的共同特征是,运行后产生一个值,但除此以外就没有其他特别的影响了;

但是有些表达式不仅仅只产生一个值,同时它们还会产生一些影响,例如修改了变量的值:

3 + (x = 4 +5);//你完全可以这样做,因为子表达式(x = 4 + 5)运算后也会产生一个值,这个值再与3相加

再来看看《C语言核心技术》对副作用的描述:

在产生一个值的过程中,表达式可能会对环境做出其他改变,这样的改变被称为副作用(side effect),诸如变量的值被修改,或者输入输出流的数据有所变化

 

为了说明这句话,我们需要举几个表达式的例子:

示例1:

x + 1;

表达式x + 1就产生了一个值,但是它没有产生一个副作用。

示例2:

x = x + 3;

表达式x = x+ 3产生了一个值,同时也会产生一个副作用。

在遇到一个序列点之前,它会完成这个更改操作

现在我们引入了一个新的概念,序列点

在程序的执行期间有一些点,在这些点中,一个特定表达式的所有副作用都会完成,而下一个表达式的副作用尚未发生。程序中这样的点被称为序列点。在两个连续的序列点之间,可以用任何次序做局部运算。作为一名程序员,你必须特别小心,不要在两个连续的序列点之间多次修改任何对象。

通俗点说就是,当表达式的执行遇到一个序列点的时候,它前面所产生的副作用都会被完成,才会继续执行下去。

但是!在执行这些副作用的过程中,顺序是不确定的!

假如一个表达式在两个序列点之间产生了A,B,C三个副作用,那么不同编译器运行的时候有些编译器有可能是先运行A,有些编译器有可能先运行B,有些编译器有可能先运行C!

这将带来一个很严重的后果,就是如果在两个序列点之间同时改变一个变量的值多次,那么在不同的编译器下运行的结果有可能不相同!

除了以上情况,还有一点,就是如果一个表达式在两个序列点之间调用了函数,这个函数的运行顺序和自变量表达式的运行顺序也是不确定的!

在《C语言核心技术》第5章:"函数调用"一小结中提到:

至于程序是以怎样的次序来计算“函数表达式”和个别“自变量表达式”,这是没有定义的。

请看示例3:

int i = 0;
printf( "%d %d\n", i, ++i );     //行为没有定义

printf中的两个自变量表达式当中,子表达式i和++i是不能确定运行顺序的,因为++i有一个副作用,如果这个副作用先运行,那么表达式i的结果就是1,否则i的结果就是0;

除了自变量表达式的执行顺序,函数本身的执行顺序也是不确定的,如果一个两个序列点之间的表达式中出现了多次函数调用的话!

请看示例4:

int x = f() + g();

该表达式产生了一个值(f() + g()),产生了3个副作用:

  • 修改x的对象为表达式结果;
  • 运行函数f;
  • 运行函数g;

如果函数f和函数g之间的执行绪互相不影响,那么它的结果是没有问题的,因为在遇到序列点之前,虽然产生了3个副作用,但是x依赖子表达式f() + g()的值,所以x的值是不需要担心的,它必然是子表达式(f() + g())的值。

然而在子表达式中,函数f和函数g之间的运行顺序是不确定的,有些编译器编译下,可能先运行f(),有些可能先运行g()!

请看示例5:

int x = 1;
x = x++;

在第二条表达式中,一共产生了两个副作用:

  • x++得出了一个值,留下一个副作用:把x的值赋值为2(因为是左递增,所以这里表达式得出来的值是x递增之后的值,2);
  • 而前面的赋值操作也留下了一个副作用:把x赋值为子表达式x++得出来的值1(因为是右递增,所以这里表达式得出来的值是x递增之前的值,1),这个副作用一旦执行,就会把变量x赋值为1;

然后呢,如果第一个副作用先执行,x++先把x的值改变为2,然后轮到第二个副作用登场了,没有错,它把x的值又赋值为1了。。。

在这次操作中,x++的递增1运算就相当于丢失了,如果不考虑序列点,表达式的运算结果就是不可预知的。

所以我们必须确保在两个序列点之间的代码,不会出现修改同一个对象多次的副作用出现。

 会出现序列点的位置

  • 在一个函数调用时,所有的自变量被计算之后,并且在执行权传递到函数语句之前。
  • 在表达式的末端,并且此表达式不是一个更大的表达式的一部分。这种完整的表达式包括:“表达式语句”内的表达式(请参考第6章“表达式语句”),for语句内的三个条件表达式、if或while语句的条件语句、return语句的表达式,以及初始化语句(initializer)。
  • 在下列运算符的第一个操作数被计算完成后:
  1. && (逻辑 AND)
  2. || (逻辑 OR)
  3. ? : (条件运算符)
  4. , (逗号运算符)

示例6:

++i < 100 ? f(i++) : (i = 0);

这个表达式是合适的,因为在第一个修改i的地方和另外两个修改i的地方之间有一个序列点。

posted @ 2019-12-21 18:29  yiyide266  阅读(697)  评论(0编辑  收藏  举报