Dennis Ritchie 在设计 C 时所作的选择之一是为编译器提供一个许可证,以便重新整理包含算术上可交换并且关联的相邻操作符(甚至出现圆括号)的表达式。这在 Kernighan 和 Ritchie 合著的《The C Programming Language》的附录中已明确指出。但是,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 程序的行为。
因此,由于无法通知此类重新分组,因此允许在任何机器上重新整理所有二元按位运算符(移位除外)。在溢出回绕的典型二进制补码机器上,可以由于相同原因重新整理涉及乘法或加法的整型表达式。