前言
- 表达式求值的顺序是由操作符的优先级和结合性决定。
- 而我们在进行表达式求值时,有些表达式的存放操作数的变量类型不同,为了能够进行运算,在求值的过程中可能需要转换为相同类型。
1. 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符(char)和短整型操作数(short),在使用之前被转换为普通整型,这种转换称为整型提升。
1.1 整型提升
整形提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
整形提升的实现过程:
char类型(1 Byte)与short类型(2 Byte)的变量在内存中不到4个字节,在运算过程中会将其先整型提升为 int类型(4Byte)。
具体方式如下:
- 将数据扩充到 32个bit 位
- 因为char类型 与 short 类型皆为有符号类型数,所以在定义时最高位默认为符号位,在整形提升时,扩充位上用符号位上的数填充。
示例:
- 负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位: 1111111
因为 char 为有符号的char, 所以整形提升的时候,高位补充符号位,即为1 提升之后的结果是: 11111111111111111111111111111111- 正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位: 00000001 因为 char 为有符号的
char 所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001- 无符号整形提升,高位补0
//示例1:
int main()
{
char a = 0Xb6;
//在内存中的存储形式:10110110(补码)
//首位默认为符号位
short b = 0Xb600;
//1011011000000000(补码)
//首位默认为
int c = 0Xb6000000;
//10110110000000000000000000000000(补码)
//以0X位前缀,表明数据为16进制
//每4个二进位表示1位16进制数位
//b为11
if(a == 0Xb6)
//变量a 整型提升为:
//11111111111111111111111110110110(补码)
//11111111111111111111111110110101
//10000000000000000000000001001010(-74)
{
printf("a\n");
}
if(b == 0Xb600)
//11111111111111111011011000000000(补码)
//11111111111111111011010111111111
//10000000000000000100101000000000(-18944)
{
printf("b\n");
}
if(c == 0Xb6000000)
{
printf("c\n");
}
//只能打印出字符 'c'
return 0;
}
//示例2:
int main()
{
char c = 1;
printf("%d\n",sizeof(c));
//1个字节
printf("%d\n",sizeof(+c));
//4个字节
printf("%d\n",sizeof(-c));
//4个字节
//只要参与运算就会整型提升
return 0;
}
1.2 算数转换
如果某个操作符的各个操作数属于不同的类型
那么,除非其中一个操作数的转换为另一个操作数的类型
否则,操作就无法进行。
下面的层次体系称为寻常算术转换:
float f = 3.14;
int i = f;
//浮点数赋值,或强制转换为整形,或精度丢失
2. 表达式运算的规则
在复杂表达式的求值过程中,计算有一套自己的运算规则(区别于不同的编译器,运算规则的细节上可能有所不同)。正是凭借这套通用规则,计算机才能实现表达式的运算求值。这套规则是由运算操作符的三个性质决定:
- 操作符的优先级
- 操作符的结合性
- 操作符是否控制求值顺序
2.1 表达式运算符的优先级与结合性
当一个表达式存在多个运算符时,这些运算符操作数的计算先后会影响表达式最后的值。因此,需要一套统一的标准来确定运算符参与运算的先后顺序,而在确定顺序后,还存在着参与运算的运算符优先级相同的情况,在这种情况下,表达式运算顺序就取决于运算符的结合性。
各运算操作符具体性质,如下表:
注:
- 操作符优先级:从上至下,优先级由高到低
- N/A 无结合性,L-R 从左至右,R-L 从右至左
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式相同 | N/A | 否 |
() | 函数调用 | rexp (rexp,…,rexp) | rexp | L-R | 否 |
[ ] | 下标引用 | rexp[rexp] | lexp | L-R | 否 |
. | 访问结构成员 | lexp.member_name | lexp | L-R | 否 |
-> | 访问结构指针成员 | rexp->member_name | lexp | L-R | 否 |
++ | 后缀自增 | lexp ++ | rexp | L-R | 否 |
– | 后缀自减 | lexp – | rexp | L-R | 否 |
! | 逻辑反 | ! rexp | rexp | R-L | 否 |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
+ | 单目,表示正值 | + rexp | rexp | R-L | 否 |
- | 单目,表示负值 | - rexp | rexp | R-L | 否 |
++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
– | 前缀自减 | – lexp | rexp | R-L | 否 |
* | 间接访问 | * rexp | lexp | R-L | 否 |
& | 取地址 | & lexp | rexp | R-L | 否 |
sizeof | 取其长度,以字节表示 | sizeof rexp sizeof(类型) | rexp | R-L | 否 |
(类型) | 类型转换 | (类型) rexp | rexp | R-L | 否 |
* | 乘法 | rexp * rexp | rexp | L-R | 否 |
/ | 除法 | rexp / rexp | rexp | L-R | 否 |
% | 整数取余 | rexp % rexp | rexp | L-R | 否 |
+ | 加法 | rexp + rexp | rexp | L-R | 否 |
- | 减法 | rexp - rexp | rexp | L-R | 否 |
<< | 左移位 | rexp << rexp | rexp | L-R | 否 |
>> | 右移位 | rexp >> rexp | rexp | L-R | 否 |
> | 大于 | rexp > rexp | rexp | L-R | 否 |
>= | 大于等于 | rexp >= rexp | rexp | L-R | 否 |
< | 小于 | rexp < rexp | rexp | L-R | 否 |
<= | 小于等于 | rexp <= rexp | rexp | L-R | 否 |
== | 等于 | rexp == rexp | rexp | L-R | 否 |
!= | 不等于 | rexp != rexp | rexp | L-R | 否 |
& | 位与 | rexp & rexp | rexp | L-R | 否 |
^ | 位异或 | rexp ^ rexp | rexp | L-R | 否 |
| | 位或 | rexp | rexp | rexp | L-R | 否 |
&& | 逻辑与 | rexp && rexp | rexp | L-R | 是 |
|| | 逻辑或 | rexp || rexp | rexp | L-R | 是 |
? : | 条件操作符 | rexp ? rexp : rexp | rexp | N/A | 是 |
= | 赋值 | lexp = rexp | rexp | R-L | 否 |
+= | 以…加 | lexp += rexp | rexp | R-L | 否 |
-= | 以…减 | lexp -= rexp | rexp | R-L | 否 |
*= | 以…乘 | lexp *= rexp | rexp | R-L | 否 |
/= | 以…除 | lexp /= rexp | rexp | R-L | 否 |
%= | 以…取模 | lexp %= rexp | rexp | R-L | 否 |
<<= | 以…左移 | lexp <<= rexp | rexp | R-L | 否 |
>>= | 以…右移 | lexp >>= rexp | rexp | R-L | 否 |
&= | 以…与 | lexp &= rexp | rexp | R-L | 否 |
^= | 以…异或 | lexp ^= rexp | rexp | R-L | 否 |
|= | 以…或 | lexp |= rexp | rexp | R-L | 否 |
, | 逗号 | rexp,rexp | rexp | L-R | 是 |
2.2 表达式求值的唯一计算路径
我们已经学习了运算操作符的相关知识,那么了解了这些知识后,就可以解决表达式求值的所有问题吗?
问题表达式,举例:
a * b + c * d + e * f
在如上的表达式计算中,我们能够确定唯一的计算顺序吗?
计算路径1:
计算路径2:
以上的两种计算顺序都符合运算的规则,可是他们的计算结果都是一致的吗?
在a,b,c,e,d,f 都为变量时,两种计算路径的所得结果确实都相同,可是,当他们为表达式,函数时,并且他们执行后会互相影响时,两种不同的运算路径所得结果就会产生差异(即,在不同编译器上就会产生不同的结果),所以,在日常的编程中,我们应该注意并避免此种情况的发生。
//示例1:
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
//示例2:
int fun()
{
static int count = 1;
return ++count;
}
//函数的调用先后顺序无法通过运算符的优先级确定
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);
return 0;
}