C语言——副作用(side effects)和序列点(sequence points)

C语言——副作用(side effects)和序列点(sequence points)

什么是副作用?

英文原文
  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.

中文翻译
  访问易失性(volatile)对象、修改对象、修改文件或调用执行上述任何操作的函数都是有副作用的,即执行环境状态的改变。

  由上述可知,副作用的本质是程序数据的改变,即导致程序数据(变量值、文件数据、寄存器数据(不包括中间临时数据)等)改变就是产生了副作用。这些副作用(程序数据的改变)会影响程序执行的结果。

  需要特别说明的是,寄存器中的临时中间运算结果并不属于执行环境状态,例如下面语句 i+1 的值对程序来说毫无用处,甚至连这条语句是否会执行也很难说,可能会被编译器优化掉。

i+1; 

  另一点需要特别说明的是,对volatile类型对象的读操作也是有副作用的。因为volatile类型对象随时可能会被外部因素(如硬件、外部程序等)改写。因此,每次使用该对象的值时,都必须进行一次直接读取,而非使用缓存的值。
  

什么是序列点?

英文原文
  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.

中文翻译
  在执行序列中某些特定的点称为序列点(sequence points),此点之前求值(evaluations)的所有副作用都应当已完成,后续求值的副作用还未发生。

  由上述可知,序列点就是代码片段中的一些特殊位置,在这些位置上,之前所有的求值均已完成,而之后的求值还未开始。最简单的序列点就是表达式语句的结尾。

int i = 0, j = 0;
-------------此处是一个序列点,上面语句的求值已完成,下面的求值还没开始
j = 2;
-------------此处是一个序列点,上面语句的求值已完成,下面的求值还没开始
i = j++ ;
-------------此处是一个序列点,上面语句的求值已完成,下面的求值还没开始
j = i * i++;
-------------此处是一个序列点,上面语句的求值已完成,下面的求值还没开始

  两个序列点之间通常会有一个或多个副作用,如语句 i = j++; 就有两个副作用,一是将 j 的旧值赋给 i ,二是将 j 的值加一。但是这两副作用的发生顺序是不确定的。

  多个副作用的发生顺序是不确定的,取决于具体的实现和优化策略。继续以 i = j++ 为例,两个副作用共有两种发生顺序:

  顺序1:先将 j 的旧值赋给 i ,再将 j 的值加一。
  大多数实现采用此顺序,因为可以节省一个CPU寄存器开销。

在这里插入图片描述
  顺序2:先将 j 的值加一,再将 j 的旧值赋给 i 。
  此顺序需要使用一个额外的CPU寄存器保存 j 的旧值,并且比顺序1多了一次数据传送操作(将 j 的旧值临时存到CPU寄存器)。前面说过,寄存器中的临时中间运算结果并不属于执行环境状态,即其不产生副作用,因此,虽然顺序2多了一次数据传送操作,但是并没有为此而多增加一个副作用。

在这里插入图片描述

序列点的特征和应用

  序列点的一个重要特征是:序列点上的执行环境状态(变量值等程序数据)是确定的。因为在序列点处,之前的求值已完成,而后面的求值还未开始。

  序列点的一个应用是——断点。我们知道很多MCU都支持仿真,仿真一般都可以设置多个断点,而在断点处我们可以查看各个寄存器的数据值,这正是因为断点应用了序列点的重要特征。

  需要特别说明的一点是,并非所有的序列点都可以设置为断点。

附录——序列点

C99标准中描述的序列点:

——在实参求值之后对函数的调用

例如:
  在对实参列表求值完成后,执行被调用函数的第一条语句前,有一个序列点,即下图红点处。在此点上,函数的实参列表已计算完毕,即先前所有的副作用均已完成,而此点之后的副作用还未开始。

在这里插入图片描述

——以下操作符的第一个操作数的末尾:逻辑与 &&;逻辑或 || ;条件运算符 ? ;逗号 ,

例如 :
  (i++)&&(++i);
  (i++)||(++i);
  (i++)?(++i):(++i);
  (i++),(++i);

  在执行 ++i 时,i++ 的副作用已完成,即 i++ 的运算结果已生效。

——完整的声明符结尾:声明符

——完整的表达式结尾:初始化表达式;表达式语句中的表达式;选择语句的控制表达式(if或switch);while或do语句的控制表达式;for语句的每个表达式;return语句中的表达式。

——紧接在库函数返回之前

——在与每个格式化输入/输出函数转换说明符相关联的操作之后

——在每次调用比较函数之前和之后,以及在任何调用比较函数和作为参数传递给该调用的对象的任何移动之间

猜你喜欢

转载自blog.csdn.net/weixin_44567318/article/details/112437143