副作用与序列点——C语言

  由于在复习 C 语言,众所周知,大学的老师热衷于类似 ++i + ++i + ++ia = b++ + (c=3) + d++ 这样的问题,这里暂且不去批判这种行为,将《C Primer Plus》中谈到的术语:副作用(side effects)与序列点(sequence point)记录一下。

副作用

  书中的解释是: 副作用(side effects)是对数据对象或文件的修改。如:
语句:

1
state = 50;

它的副作用是将变量的值设置为 50。副作用?这似乎更像是主要目的,但是从 C 语言的角度看,主要目的是对表达式求值。给出表达式4 + 6,C会对其求值得10;给出表达式 states = 50,C 会对其求值得 50。对该表达式求值的副作用是把变量 states 的值改为50。跟赋值运算符一样,递增和递减运算符也有副作用,使用它们的主要目的就是使用其副作用。

序列点

  书中的解释是: 序列点(sequence point)是程序执行的点,在该点上,所有的副作用都在进入下一步之前发生。在 C语言中,语句中的分号标记了一个序列点。意思是,在一个语句中,赋值运算符、递增运算符和递减运算符对运算对象做的改变必须在程序执行下一条语句之前完成。后面我们要讨论的一些运算符也有序列点。另外,任何一个完整表达式的结束也是一个序列点。
  所谓完整表达式(full expression)就是指这个表达式不是另一个更大表达式的子表达式。例如,表达式语句中的表达式和 while 循环中的作为测试条件的表达式,都是完整表达式。
  序列点概念的存在有利于分析后缀递增何时发生,如:

1
2
3
4
while(guest++ < 10)
{
printf("%d \n",guest);
}

  对于上面的代码,许多人都会认为“先使用值后递增”的意思是,在 printf() 中先使用 guest,再递增它。事实上,表达式 guest++ < 10 是一个完整的表达式,因为它是 while() 的测试条件,是属于完整表达式范畴,所以在该语句的结束就是一个序列点,因此 C 保证了在程序执行到 printf() 之前发生副作用(即递增 guest),同时,由于是后缀形式,所以保证了 guest 在完成与 10 比较后才进行递增。
  现在来看这句:

1
y = (4 + x++) + (6 + x++);

  表达式 (4 + x++) 不是一个完整的表达式,所以在子表达式 (4 + x++) 执行完后不会立刻对 x 进行递增操作,(6 + x++) 同理。这里,完整表达式是整个赋值语句,分号标记了序列点,所以,C 只是保证在该赋值语句完成后对 x 进行两次递增操作,并未指明是在对子表达式求值以后递增 x,还是对所有表达式求值后再递增 x。因此,要尽量避免编写类似的语句。

其他

  C语言标准对副作用和序列点的定义如下:

Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.

翻译: 访问易变对象,修改对象或文件,或者调用包含这些操作的函数都是副作用,它们都会改变执行环境的状态。计算表达式也会引起副作用。执行序列中某些特定的点被称为序列点。在序列点上,该点之前所有运算的副作用都应该结束,并且后继运算的副作用还没发生。