深入理解C语言-----副作用(Side Effect)和顺序点(Sequence Point)

来源:互联网 发布:淘宝退款不退货规则 编辑:程序博客网 时间:2024/06/04 18:00

先看几个概念:

1。引用透明:如果一个表达式(或子表达式)只计算出值而不改变环境,我们就说它是引用透明的,这种表达式早算晚算对其他计算没有影响(不改变计算的环境。当然, 它的值可能受到其他计算的影响)。比如:(a+b)*(c+d),无论先计算乘号两边都可以

2.  副作用:如果一个表达式不仅算出一个值,还修改了环境,就说这个表达式有副作用(因为它多做了额外的事)。比如:a++ 


那么,多个副作用之间的发生顺序是怎样的?

       C 标准规定代码执行过程中的某些时刻是Sequence Point,当到达一个Sequence Point时,在此之前的Side Effect 必须全部作用完毕,在此之后的Side Effect 必须一个都没发。至于两个Sequence Point之间的多个Side Effect 哪个先发生哪个后发生则没有规定,编译器可以任意选择各Side Effect 的作用顺序。

       解释一下就是,在目标的代码里,对同一元素的多次访问(内存的访问)必然通过几段独立代码完成。现代计算机的计算都在寄存器里做,顺序点的作用就是确保在某个时刻这些改变必须反应到随后对同一存储位置的访问中。


看看常见顺序点的位置

      1. 每个完整表达式结束时。完整表达式包括变量初始化表达式,表达式语句,return 语句的表达式,以及条件、循环和 switch 语句的控制表达式(for 头部有三个控制表达式)
      2. 运算符 &&、||、?: 和逗号运算符的第一个运算对象计算之后; 
      3. 函数调用中对所有实际参数和函数名表达式(需要调用的函数也可能通过表达式描述)的求值完成之后(进入函数体之前)。 

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

      5.像printf 、scanf这种带转换说明的输入/ 输出库函数,在处理完每一个转换说明相关的输入/ 输出操作时是一个Sequence Point。
      6.库函数bsearch和qsort在查找和排序过程中的每一步比较或移动操作之间是一个Sequence Point。

标准截图:



这样一来,老谭绿皮书上的一个错误就显而易见了,《C程序设计》第三版64页中,有这么一个表达式 a+=a-=a*a,在a的初值为12时,该表达式的结果可能是-120也可能是-264.

原因就留给各位读者了哈。事实上,如果用gcc的wall选项编译可以看到 operation 'a' may not be defined 的警告。正是基于这些原因使得不少公司的类似的笔试题都被专业人士鄙为恶趣味。


        C/C++ 语言的做法完全是有意而为,其目的就是允许编译器采用任何求值顺序,使编译器在优化中可以根据需要调整实现表达式求值的指令序列,以得到效率更高的代码。像Java 那样严格规定表达式的求值顺序和效果,不仅限制了语言的实现方式,还要求更频繁的内存访问(以实现副作用),这些可能带来可观的效率损失。应该说,在这个问题上,C/C++和 Java 的选择都贯彻了它们各自的设计原则,各有所获(C/C++ 潜在的效率,Java 更清晰的程序行为)


       说这么多对我们写代码的指导意义是,在两个Sequence Point之间,同一个变量的值只允许被改变一次。另外,如果在两个Sequence Point之间既要读一个变量的值又要改它的值,只有在读写顺序确定的情况下才可以这么写。举个例子,同样是只改变变量一次,同样是等号左边写,等号右边读,i = i + i 是可行的,而a[i++] = i 的结果就是未定义的。


参考文章,裘宗燕教授的C/C++语言中的表达式求值

1 0