K&R C 为编译器提供一个许可证,以便重新整理包含算术上可交换并且关联的相邻运算符(甚至出现圆括号)的表达式。但是,ISO C 没有给编译器同样的自由。
本节通过考虑以下代码片段中的表达式语句,讨论这两个 C 定义之间的差异,并阐明表达式的副作用、分组以及求值之间的差别。
int i, *p, f(void), g(void); /*...*/ i = *++p + f() + g();
表达式的副作用是修改内存并访问 volatile 限定对象。示例表达式的副作用是更新 i 和 p 以及 f() 和 g() 函数中包含的任何副作用。
表达式的分组是值与其他值和运算符相结合的一种方式。示例表达式的分组主要是加法的执行顺序。
表达式的求值包括生成结果值所必需的所有运算。要对表达式求值,所有指定的副作用必须在上下两个序列点之间发生,并且使用特定的分组执行指定的操作。对于示例表达式,更新 i 和 p 必须发生在前一语句之后并且通过该表达式语句的 ; 执行。函数调用可以在前一语句之后按任一顺序、任何时间执行,但必须在使用其返回值之前。特别地,在使用操作的值之前,导致内存更新的运算符不需要分配新值。
由于加法在算术上可交换并且关联,因此 K&R C 重新整理许可证适用于该示例表达式。为了区别常规圆括号和表达式的实际分组,左、右花括号指定分组。表达式的三种可能的分组为:
i = { {*++p + f()} + g() }; i = { *++p + {f() + g()} }; i = { {*++p + g()} + f() };
给定 K&R C 规则,所有这些分组均有效。此外,即使表达式是按以下任意方式编写的,所有这些分组仍有效:
i = *++p + (f() + g()); i = (g() + *++p) + f();
如果在溢出导致异常的体系结构上对该表达式求值,或者加法和减法在溢出时不是互逆运算,则当一个加法运算溢出时,这三种分组表现不同。
对于这些体系结构上的此类表达式,K&R C 中唯一可用的求助措施是分割表达式以强制进行特定的分组。以下可能的重写分别强制执行以上三种分组:
i = *++p; i += f(); i += g() i = f(); i += g(); i += *++p; i = *++p; i += g(); i += f();
对于算术上可交换并且关联但实际上在目标体系结构上并非如此的操作,ISO C 不允许进行重新整理。因此,ISO C 语法的优先级和关联性完整描述了所有表达式的分组。所有表达式在进行解析时必须进行分组。所考虑的表达式按以下方式分组:
i = { {*++p + f()} + g() };
此代码仍不表示必须在 g() 之前调用 f(),也不表示必须在调用 g() 之前增大 p。
在 ISO C 中,不需要为了避免意外的溢出而分割表达式。
由于不完全的理解或不准确的表示,ISO C 经常被错误地描述为支持圆括号或根据圆括号求值。
由于 ISO C 表达式的分组通过自身的解析指定,括号仅充当控制如何解析表达式的一种方法。表达式的自然优先级和关联性具有与括号完全相同的权重。
按如下所示写入的前一表达式可能不会对其分组或求值造成其他影响。
i = (((*(++p)) + f()) + g());
K&R C 重新整理规则的一些原因包括:
重新整理为优化提供了更多的机会,如编译时常量折叠。
在大多数机器上,重新整理不会更改整型表达式的结果。
在所有机器上,一些操作在算术上和计算上可交换并且关联。
ISO C 委员会最终确认:重新整理规则应用于所描述的目标体系结构时,本来是要作为 as if 规则的一个实例。ISO C 的 as if 规则是通用许可证,它允许实现任意偏离抽象机器描述,只要偏离不更改有效 C 程序的行为。
由于无法通知此类重新分组,因此允许在任何机器上重新整理所有二元按位运算符(移位除外)。在溢出回绕的典型二进制补码机器上,可以由于相同原因重新整理涉及乘法或加法的整型表达式。