前言
本文将抽丝剥茧地讲述在表达式求值时因为操作数类型的不同而存在的整型提升和算术转换;以及操作符的属性
注:在阅读本文前建议先看看博主写的篇目5(链接在这:http://t.csdnimg.cn/u7uje),对于理解本篇文章十分有帮助
一、为什么会发生整型提升或者算术转换?
思考:在使用表达式求值时,例如:
代码如下:
#include<stdio.h>
int main()
{
char a = 2;
int b = 3;
char c = a + b;
printf("%d\n", c);
return 0;
}
代码运算结果如下:
这样看着似乎 char 类型的a + int 类型的 b 的结果放入 char 类型的c 可以直接 2+3就得到答案5;
但是,实际情况真的如此吗?
我们再看个例子:
代码如下:
#include<stdio.h>
int main()
{
char a = 2;
int b = 126;
char c = a + b;
printf("%d\n", c);
return 0;
}
代码运行结果如下:
显然,我们可以看到简单的数字相加 2+126 = 128 ; 可是程序的输出结果却为 -128 .是不是很疑惑呢?在分析之前,我们先来了解一下关于计算机进行计算的硬性条件:
注:CPU是计算机系统的核心,负责执行程序指令、处理数据和控制其他硬件设备。它通常由控制单元、算术逻辑单元(ALU)和寄存器组成。CPU的设计使其能够处理各种类型的计算任务,包括复杂的逻辑运算和数据处理。CPU的通用性使其成为执行操作系统和应用程序的理想选择
我们知道计算是在计算机的CPU相应的运算器件内执行的,而CPU的整型运算器(ALU)的操作数的字节长度一般都是int 的字节长度,同时也是CPU的通用寄存器的长度。所以,即使两个char类型相加,在CPU执行式时实际上也要先转换为CPU内整型操作数的标准长度。
上面这部分用人话讲就是:计算机的计算是在CPU中进行的,针对整型的计算,CPU中专门有一个整型运算器(ALU)来进行整型的相关运算,而寄存器是用来存储数据的集成在CPU上的硬件,通过让整型运算器直接从寄存器中拿数据的方式,从而提高计算机计算的效率;然而,CPU在执行整型计算的任务时,其执行的整型操作数最小长度为int 类型,也就是 4 Byte。
注:寄存器:CPU作为中央处理器,计算的数据是从寄存器中取得的。当寄存器在处理某些数据的时候,内存中的数据就可以往高速缓存中拿,而高速缓存中的数据也可以往寄存器中拿(保证大部分的数据能在寄存器中被找到)-->这样就可以大大提升CPU的计算速度
通用CPU(general-purpose CPU)是难以实现两个char 类型的数据直接相加运算(虽然机器指令中可能有这种字节相加的指令)
所以,表达式中小于 int 类型长度的整型值想要在CPU中进行计算,就必须先转换为 Int 或者 unsigned int 。
注:char 类型属于整型类型,因为字符存储在内存中本质用的是ASCII码值,ASCII码值就是整数,故而char 类型属于整型家族(整型家族在内存中存的二进制序列是补码)
所以,当表达式中计算的两个数据其中有一个在内存中所占用的空间小于 Int 类型的4 byte,就会发生整型提升
可能看到这你又有疑问了,如果表达式中的操作数的类型在内存中所占的空间大于4byte呢?此时该如何处理?
类型排名:
注:从上到下,排名依次降低
如果表达式中的操作数的类型在内存中所占的空间大于4byte--> 根据操作数的排名,将低的类型向排名高的类型转换--> 向上转换类型
以上便是在CPU计算时要对不同类型的操作数进行整型提升或者算术转换的原因。
二、整型提升
整型提升的规则:
1、有符号整型的提升:按照变量类型的符号位来提升的
2、无符号整型的提升:高位补0
例1:
#include<stdio.h>
int main()
{
char a = 2;
int b = 3;
char c = a + b;
printf("%d\n", c);
return 0;
}
代码运行结果:
分析:char a = 2; --> char 类型在内存中所占的空间为 1 Byte ,也就是 8 Bit,变量a 的补码占8个二进制位 ;计算是在CPU中进行的,而CPU处理的最小整型为 4Byte,所以在计算之前,要对变量a 进行整型提升 ,而变量b的类型为int ,满足CPU进行整型计算的最小长度,所以不用进行整型提升这一操作。
此处的char 类型为有符号整型 --> 其整型提升是按照变量类型的符号位来提升的
将a+b计算的结果存放在变量c中,而c的类型为char ,只有8 bit位,显然是放不下32bit 位,所以会发生截断,即c存放的内存中的补码:00000101
由于正整数的原码、反码、补码相同,所以输出为 5;
例2:
#include<stdio.h>
int main()
{
char a = 2;
int b = 126;
char c = a + b;
printf("%d\n", c);
return 0;
}
代码运行结果如下:
注:有符号char类型数据表示的范围为 -128~127 。但ASCII码值的取值范围为0~127以表示128个字符。原因:char类型所占内存空间为1byte,相当于8bit,而整数的二进制序列有三种表现形式:原码、反码、反码。符号位(二进制序列左边第一位为符号位)只表示数据的正负,故而只有剩下7个二进制位来表示数据的大小;因为补码的存在(在计算机中,补码用于计算)使得0只能用0000 0000 来表示,而不存在1000 0000 来表示-0;在补码中最大:0111 1111--> 十进制为127 ,最小:1000 0000 --> 十进制为 -128
分析:变量a 参与了计算,a的类型为char,小于CPU整型计算的最小长度,所以要进行整型提升。而将a+b 32位bit的结果放在只有 8位bit char 类型的变量c 中,会发生截断
截断之后,存放在c中的补码:1000 0000
反码:(在补码的基础上-1)0111 1111
原码:(在反码的基础上,符号位不变,其他位按位取反)1000 0000
所以输出为 -128
下面的例子,可以更加让我们更清楚地看到数据的整型提升。
注:有符号整型的提升是根据其符号位来补的;无符号整型的提升直接补0
例三:
#include<stdio.h>
int main()
{
char a = 0xb6;//两个十六进制数代表1byte
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
代码运行结果:
分析:只要在表达式中使用 char 、short等长度小于int 类型类型,在使用中都要进行整型提升(有符号整型的提升是按照变量类型的符号位来提升的)显然,变量a 在参与表达式 a == 0xb6 的判断时,CPU会将a 整型提升为 0xbbbb bbb6来与0xb6比较是否相等,显然两者不同,所以a == 0xb6 为假;b同理;而变量c 本身就为int 类型,无需进行整型提升,所以 c == 0xb6000000为真,故而输出c
注:此处若为 unsigned char a= 0xb6; unsigned short b = 0xb600;就会输出a、b,因为无符号整型在整型提升时为高位补0,补完之后其大小前后无区别,满足输出a、b的判断条件;代码如下:
#include<stdio.h>
int main()
{
unsigned char a = 0xb6;//两个十六进制数代表1byte
unsigned short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
代码运行结果为:
关于整型提升,长度小于int 类型长度的类型数据,只要参与运算,就会发生整型提升;我们再看一个例子:
例4:
#include<stdio.h>
int main()
{
char a = 1;
printf("%zu\n", sizeof(a));
printf("%zu\n", sizeof(+a));
printf("%zu\n", sizeof(-a));
return 0;
}
代码输出结果如下:
分析:+、- 为双目操作符;+a、-a --> a参与了表达式的计算,发生了整型提升,故而sizeof(+a)=4; sizeof(-a) = 4;
三、算术转换
算术转换的条件:如果某个操作符的各个操作数属于不同的类型(都大于或等于 4Byte),得将这些数转换为同类型,才能进行计算;
算术转换的规则:向上转换类型
类型排名:
例1:
#include<stdio.h>
int main()
{
int a = 1;
double b = 2;
printf("%zu\n", sizeof(a + b));
return 0;
}
代码运行结果:
显然此处,因为double类型所占内存空间为8 byte,而int 类型所占内存空间为 4bit,在CPU中计算需要统一操作数的类型,由于double类型高于 int 类型,所以将变量a进行了算术转换,a+b 的结果也为double 类型;所以sizeof(a + b ) = 8;
四、操作符的属性
1、影响复杂表达式求值的三个影响因素:
1、操作符的优先级
2、操作符的合法性
3、是否控制求值顺序
注:相邻两个操作符先执行哪一个,取决于他们的优先级;如果他们两个的优先级相同就看他们的结合性;
优先级讨论的是相邻的两个操作符;
结合性:相邻操作符优先级相同的情况下,先算谁。
即使我们掌握了操作符优先级、结合性的知识,也不一定能完全控制求值顺序等属性;即任然存在一些表达式无法通过优先级和结合性来确定它的唯一计算路径,我们将这些表达之称为问题表达式。
操作符优先级和结合性的表格如下图:
2、一些问题表达式:
例1:
a*b+c*d+e*f
注:上述表达式在计算的时候,由于*的优先级比+高,所以会先计算乘法部分,但是优先级只能决定相邻操作符的执行先后,故而无法保证第三个*是否比第一个+要先执行;此时便存在了计算路径不唯一这一现象,而若a、b、c、d、e、f 为表达式,他们之间谁先执行就会影响到后面数据的变化,故而存在问题;
注:往后写表达式一定要避免写得太复杂
上述表达式在计算机中的计算顺序:
计算顺序1:
a*b
c*d
a*b+c*d
d*f
a*b+c*d+e*f
计算顺序2:
a*b
c*d
e*f
a*b+c*d
a*b+c*d+e*f
例2:
int c = 2;
int b = c+ --c;
由上优先级的图表可以得知,-- 的优先级比+ 高,所以先执行--c,而后再+ ;但是不知 +操作符对左操作数的获取在右操作数之前还是之后;这里便存在歧义:
歧义1:
若+操作符 对左操作数的获取在右操作数之前:(+ 左边操作数c 的值不被 --c 导致c的改变而影响)
int b = 2 + 1; 即 b = 3;
歧义2:
若+ 操作符对左操作数的获取在右操作数之后:(+ 左边操作数c 的值会被 --c 导致c的改变而影响)
--c --> c= 1;
int b = 1+1; 即 b = 2;
例3:
代码如下:
#include<stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("i=%d\n", i);
printf("ret=%d\n", ret);
return 0;
}
例3这个代码存在的问题有点像例1表达式与例2表达式存在问题的集合体;
歧义:优先级是相邻两个操作符之间的,在此处就不清楚第一个+ 与第三个(++i)谁先计算了;且,不知 ++操作符对其操作数 i 的获取是怎么样的;
计算顺序歧义1:
++i
++i
(++i) + (++i)
++i
(++i) + (++i) + (++i)
计算顺序歧义2:
++i
++i
++i
(++i) + (++i)
(++i) + (++i) + (++i)
++操作符对其操作数 i 的获取歧义1:(拿最后一个(++i)得到的 i 的值来计算)
++i --> i = 2;
++i --> i = 3;
++i --> i = 4;
(++i) + (++i) --> 8
(++i) + (++i) + (++i) --> 12
++操作符对其操作数 i 的获取歧义2:(拿第二个(++i)得到的 i 的值来计算)
++i --> i = 2;
++i --> i = 3;
(++i) + (++i) --> 6
++i --> i = 4;
(++i) + (++i) + (++i) --> 10
总结
1、为什么存在整型提升与算术转换?
因为CPU计算整型数据的最小长度为 4Byte,且是不同类型的数据在内存中所占的空间不同,就会导致其二进制序列的位数不同,为了利于计算,当操作符的各个操作数的类型(大于4Byte)不同时,就会发生算术转换.
2、整型提升:
有符号整型的提升:用其符号位来填补
无符号整型的提升:直接补0
3、算术转换:向上转换类型
4、操作符的属性:操作符有优先级和结合性。
优先级:相邻两个操作符之间谁先计算
结合性:在优先级相同的情况下(谁先算)的计算顺序
注:当知道了操作符的优先级与结合性,可能并不能完全得知一个表达式的运算顺序;当一个表达式的运算顺序不唯一时,我们称此表达式为问题表达式。