zoukankan      html  css  js  c++  java
  • [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
    ", 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的地方之间有一个序列点。

  • 相关阅读:
    x64 平台开发 Mapxtreme 编译错误
    hdu 4305 Lightning
    Ural 1627 Join(生成树计数)
    poj 2104 Kth Number(可持久化线段树)
    ural 1651 Shortest Subchain
    hdu 4351 Digital root
    hdu 3221 Bruteforce Algorithm
    poj 2892 Tunnel Warfare (Splay Tree instead of Segment Tree)
    hdu 4031 Attack(BIT)
    LightOJ 1277 Looking for a Subsequence
  • 原文地址:https://www.cnblogs.com/yiyide266/p/12061159.html
Copyright © 2011-2022 走看看