表达式(一)
一、概述
表达式(expression)是由运算符(operator)和操作数(operand)构成的序列,可用于值计算、指定对象或者函数、生成副作用,或者执行上述行为的组合。运算符操作数的值计算应在运算结果的值计算之前。
number = func(3) + func(5);
func(3)、func(5)函数调用应在加法运算之前完成。
如果标量对象的某个副作用相对于同一标量对象的其它副作用或者使用同一标量对象值进行值计算是无序的,则其行为是未定义的。
i = ++i + 1; //未定义行为。 a[i] = i++; //未定义行为。
存储赋值不需要到下个序列点(sequence point)才进行。如果一个表达式中不止一个操作数包含了对同一对象的左值引用(lvalue referencing),并且至少一个操作数对该对象发生了副作用,则该表达式是歧义表达式(ambiguous expression)。歧义表达式的值取决于其副作用评估的先后顺序,因此上述两个表达式都是歧义表达式,其行为是未定义的;但下面表达式的行为是明确的。
i = i + 1;
a[i] = i;
如果一个表达式的子表达式存在多种允许排序,那么在任何一种排序中如果出现这种未排序的副作用,其行为是未定义的。除非特殊说明,子表达式的副作用和值计算都是无序的。对于程序执行过程中多次评估的表达式,其子表达式的未排序评估和不确定排序的评估每次评估时不需要一致执行。
语法(syntax)规定了运算符在表达式求值时的优先级(precedence)和结合性(associativity)。运算符和操作数的分组根据语法确定。
位运算符(bitwise operators)包括一元运算符~,二元运算符<<、>>、&、^和|,这些运算符要求操作数必须是整数。这些运算符生成的值取决于整数的内部表示;对于有符号整数,存在实现定义的方面和实现未定义的方面。
如果表达式在求值过程中出现异常情况(exceptional condition),即结果不是数学定义的或者不在其类型可表示的值域范围内,其行为是未定义的。
访问对象存储值时,对象有效类型(effective type)是对象声明时的类型(注:分配的对象不存在声明类型。)。
使用内存管理函数(例如:malloc、calloc等函数。)创建的对象是未声明类型的对象。
如果通过非字符类型左值(lvalue)将值存储到未声明类型的对象中,那么对于该次访问以及不修改存储值的后续访问,左值(lvalue)类型是该对象有效类型。
如果调用函数(例如:memcpy、memmove。)将值复制到一个未声明类型对象,或者将值作为字符类型数组复制到一个未声明类型对象,那么对于该次访问以及不修改存储值的后续访问,被复制值的对象的有效类型(如果存在)是被修改对象的有效类型。
对于其它未声明类型的对象的访问,对象的有效类型是访问时使用的左值类型。
编译器可能使用基于类型的别名信息(type-based aliasing information)对生成的代码进行优化。ANSI别名规则规定:指针只能解引用相同类型或者兼容类型的对象。ISO/IEC 9899:2024标准规定:对象的存储值只能通过具有以下类型之一的左值表达式(lvalue expression)访问:
-- 与对象有效类型兼容的类型。
int i = 5; int *ptr = &i; i = *ptr + 1;
-- 与对象有效类型兼容类型的限定版本。
int i = 5; const int *ptr = &i; i = *ptr + 1;
-- 与对象有效类型的底层类型兼容的有符号或者无符号类型。
int i = 5; unsigned int *ptr = &i; i = *ptr + 1;
-- 与对象有效类型的底层类型限定版本兼容的有符号或者无符号类型。
int i = 5; const unsigned int *ptr = &i; i = *ptr + 1;
-- 聚合类型(aggregate type)或者联合类型(union type),其包含具有上述类型之一的成员(递归地包含子聚合成员或者包含联合成员。)。
union {int *pi; float *pf;} u; ... int i = 5; u.pi = &i; i = *u.pi + 1;
-- 字符类型。
int i = 90; size_t size = sizeof(int); signed char *ptr = (signed char *)&i; for(size_t u=0; u<size; u++) printf("%#X ", *(ptr+size-1-u)); //输出0 0 0 0X5A。
浮点表达式可能会被收缩,也就是说整个表达式会作为单个操作来评估,从而忽略源代码和表达式评估方法所带来的舍入误差。收缩表达式(contracted expression)的中间操作视为具有无限范围和精度,但最终结果会舍入为表达式评估方法所确定的格式。收缩表达式可能会忽略浮点异常。
(注:表达式收缩是指将多个算术操作合并成单个指令(例如:fma),从而减少中间结果的存储次数,例如:表达式(x × y) + z可优化为一个步骤。)
<math.h>头文件中的编译提示FP_CONTRACT用于允许或者禁止收缩表达式;否则表达式是否可以收缩以及如何收缩将由实现定义。如果FP_CONTRACT编译提示状态是ON,则允许收缩表达式;如果其状态是OFF,则禁止收缩表达式。
在允许收缩表达式的情况下,编译器将浮点表达式收缩成较少的机器码进行运算,这样可以提高执行效率;但可能损失精度、破坏程序的可预测性。
涉及十进制浮点类型的运算符根据ISO/IEC 60559标准的语义进行评估,包括按照ISO/IEC 60559标准规定生成具有首选指数的结果。