1、 隐式类型转换 —— 整型提升
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
为什么要进行整型提升
-
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
-
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
-
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度,一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
-
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
如何进行整型提升
—— 整型提升是按照标量的数据类型的符号位来提升的
//负数的整型提升
char c1 = -1;
10000000000000000000000000000001 —— 原码
11111111111111111111111111111110 —— 补码
11111111111111111111111111111111 —— 补码
变量c1的二进制位中只有8个比特位 —— 发生截断
11111111 —— char类型c1的储存
因为char为有符号char —— 所以整型提升的时候 —— 高位补充符号位(1)
11111111111111111111111111111111 —— 整型提升后的结果
可以利用整型提升之后的结果进行运算 —— 注意:内存中储存的是补码
//正数的整型提升
char c2 = 1;
00000000000000000000000000000001 —— 原码、补码、反码
变量c2的二进制位中只有8个比特位 —— 发生截断
00000001 —— char类型c2的储存
因为char为有符号char —— 所以整型提升的时候 —— 高位补充符号位(0)
00000000000000000000000000000001 —— 整型提升后的结果
//无符号数的整型提升,高位补0
整型提升的例子:
#include<stdio.h>
int main()
{
char a = 3;
//00000000000000000000000000000011 —— 原码、反码、补码
//00000011 —— 截断之后的储存存值 —— 补码
//00000000000000000000000000000011 —— 整型提升之后 —— 可进行运算
char b = 127;
//00000000000000000000000001111111 —— 原码、反码、补码
//01111111 —— 截断
//00000000000000000000000001111111 —— 整型提升之后
char c = a + b;
//00000000000000000000000010000010 —— 运算之后
//10000010 —— 截断之后
//11111111111111111111111110000010 —— 整型提升 —— 补码
//11111111111111111111111110000001 —— 反码
//10000000000000000000000001111110 —— 原码 —— -126
printf("%d\n", c); //打印值为-126
return 0;
}
例题:
//实例1
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
答案为:c
解析:
变量a为字符类型 —— 需要进行整型提升 —— a为10110110 —— 最高位为符号位(1)
—— 整型提升之后 —— 11111111111111111111111110110110 —— 整型提升之后不等于原值
变量b同理 —— 整型提升之后不等于原值
变量c为整型 —— 不需要进行整型提升 —— 等于原值。
//实例2
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
答案为:1 4 4
分析:
第一个:c为char类型的变量,所以大小为1个字节
第二个:+ 为运算操作符 —— 如果运算就需要进行整型提升 —— 整型提升到int类型 —— 大小为4字节
第三个;和第二个同理
2、 算术转换
- 前面我们讲的整型提升是:int类型大小以下及int类型运算类型的转换。
- 那么如果是int类型大小以上的运算要怎么转化呢?
- 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
具体是怎么转换的现在先不讲 —— 因为我们现在还不知道浮点型在内存中时怎么存储的。
3、 操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
操作符优先级
操作符 | 描述 | 结合性 | 是否控制求值顺序 |
---|---|---|---|
( ) | 聚组 | N/A | 否 |
( ) | 函数调用 | L - R | 否 |
[ ] | 下标引用 | L - R | 否 |
. | 访问结构成员 | L - R | 否 |
-> | 访问结构指针成员 | L - R | 否 |
++ | 后缀自增 | L - R | 否 |
– | 后缀自减 | L - R | 否 |
! | 逻辑反 | R - L | 否 |
~ | 按位取反 | R - L | 否 |
+ | 单目,表示正值 | R - L | 否 |
- | 单目,表示负值 | R - L | 否 |
++ | 前缀自增 | R - L | 否 |
– | 前缀自减 | R - L | 否 |
* | 间接访问 | R - L | 否 |
& | 取地址 | R - L | 否 |
sizeof | 去其长度,以字节表示 | R - L | 否 |
(类型) | 类型转换 | R - L | 否 |
* | 乘法 | L - R | 否 |
/ | 除法 | L - R | 否 |
% | 取余 | L - R | 否 |
+ | 加法 | L - R | 否 |
- | 减法 | L - R | 否 |
<< | 左移操作符 | L - R | 否 |
>> | 右移操作符 | L - R | 否 |
> | 大于 | L - R | 否 |
>= | 大于等于 | L - R | 否 |
< | 小于 | L - R | 否 |
<= | 小于等于 | L - R | 否 |
== | 等于 | L - R | 否 |
!= | 不等于 | L - R | 否 |
& | 位于 | L - R | 否 |
^ | 位异或 | L - R | 否 |
\ | 位与 | L - R | 否 |
&& | 逻辑与 | L - R | 是 |
\\ | 逻辑或 | L - R | 是 |
? : | 条件操作符 | N\A | 是 |
= | 赋值 | R - L | 否 |
+= | 以…加 | R - L | 否 |
-= | 以…减 | R - L | 否 |
*= | 以…乘 | R - L | 否 |
/= | 以…除 | R - L | 否 |
%= | 以…取余 | R - L | 否 |
<<= | 以…左移 | R - L | 否 |
>>= | 以…右移 | R - L | 否 |
&= | 以…与 | R - L | 否 |
^= | 以…异或 | R - L | 否 |
\= | 以…或 | R - L | 否 |
, | 逗号 | L - R | 是 |
其中与 \ 代替 |(与) 操作符,因为 | 在markdown的表格中打不出来
其中的L - R表示的是从左到右运算。(left - right)
一些有问题表达式
表达式1
//表达式的求值部分由操作符的优先级决定。
a*b + c*d + e*f
注释:代码1在计算的时候 —— 由于*比+的优先级高 —— 只能保证*的计算是比+早
—— 但是优先级并不能决定第三个*比第一个+早执行。
有以下两种计算可能:
第一种:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
第二种:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
表达式2
c + --c;
注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面
—— 但是我们并没有办法得知操作符的左操作数的获取在右操作数之前还是之后求值
—— 所以结果是不可预测的,是有歧义的。
表达式3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表达式3在不同编译器中测试结果:非法表达式程序的结果
表达式4
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
答案为(在VS2019编译器中):2 - 3 * 4 = - 10
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
表达式5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
这段代码中的第一个 + 在执行的时候 —— 第三个++是否执行 —— 这个是不确定的 —— 因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
在 VS2019 中和Linux环境中的结果不一样
在 VS2019 中
答案:12 4
过程:4 + 4 + 4 = 12
在Linux环境中
答案:10 4
过程:2 + 3 + 4 = 10
总结:
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。