我们知道,整数在计算机中有三种表示方法,即原码、反码、补码。要了解整型提升规则,我们首先要了解原码、反码、补码的基本概念。
原码、反码、补码均分为符号位和数值位两部分,符号位为最高位,“1”表示负数,“0”表示正数。在有符号数中,正整数的原码、反码、补码相同;负整数的原码为十进制正数转换后的二进制数,且最高位为1,负整数的反码为原码的最高位不变,其他位取反,负整数的补码则是在此基础上加“1”。
那么计算机中为什么要存在原码、反码、补码呢?可以举出一个例子加以理解,我们知道计算机的CPU是只有加法器的,那么计算机如何实现减法运算呢?我们看下面一段代码:
#include <stdio.h>
int main()
{
int a = 20;
int b = -10;
int c = a + b;
return 0;
}
对于变量a、b,其所开辟的空间中存放的是数据“20”和“-10”的补码,“20”为正整数,所以其原码、反码、补码相同,即“0000 0000 0000 0000 0000 0000 0001 0100”,“-10”的原码为:“1000 0000 0000 0000 0000 0000 0000 1010”,“-10”的反码为:“1111 1111 1111 1111 1111 1111 1111 0101 ”,“-10”的补码为:“1111 1111 1111 1111 1111 1111 1111 0110”。
将“20”与“-10”的补码相加,为:“1 0000 0000 0000 0000 0000 0000 0000 1010”。由于int型数据所占内存大小只有4个字节,且一般PC存储数据模式为小端模式,即数据的低位保存在内存的低地址中,数据的高位保存在内存的高地址中,所以“1 0000 0000 0000 0000 0000 0000 0000 1010”中的最高位“1”被舍弃,留下数据“0000 0000 0000 0000 0000 0000 0000 1010”,即十进制“10”,这就是计算机进行减法运算的规则,通过这个例子我们也可以知道原码、反码、补码到底有什么用处,当然原码、反码、补码的作用还远不止于此。
上面都是在介绍原码、反码、补码的基本概念,接下来进入正题,开始介绍整型提升的规则。首先来看一段代码:
#include <stdio.h>
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c);
return 0;
}
我们知道,char类型也是int类型的一种,但char类型所开辟的空间只有一个字节大小,显然是无法存放int型的32位数据的,但是输出时却是“%d”的整型数据类型,所以在操作char类型数据的运算时,需要将数据进行整型提升,提升为int型的32位数据来进行计算。整型提升的规则是:先将十进制数据进行转换为八位的二进制数据,如“3”转换为:“0000 0011”,接着,再将八位数据“提升”到32位。提升规则为:若最高位为“0’,则补全数据全为0;若最高位为”1“,则补全数据全为1,则”3“、”127“整型提升后的补码分别为:“0000 0000 0000 0000 0000 0000 0000 0011”、“0000 0000 0000 0000 0000 0000 0111 1111”,由于3和127都是正整数,所以原码、反码、补码相同。将“3”和“127”的补码相加得“0000 0000 0000 0000 0000 0000 1000 0010”。将所得数据存入c变量,取“1000 0010”,所以c的补码为1000 0010,但这仅仅是c的补码,我们需要再次对c的补码进行整型提升,得到c的原码才能求出输出打印结果。
通过前面所讲的规则,c的补码为:“1111 1111 1111 1111 1111 1111 1000 0010”,可知c的原码为:“1000 0000 0000 0000 0000 0000 0111 1110”,此时可知原码对应的打印结果为-126。
这个只是大概说清楚了整型提升的基本概念以及原码、反码、补码之间的转换,但是对于初学者来说,还是很容易被“char”、“int”、“有符号数”和“无符号数”等概念弄得晕头转向,若要理清它们之间的关系,我们还需要多做练习。
练习1:
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d,b=%d,c=%d", a, b, c);
return 0;
}
从这段代码来看,我们很容易知道a、b、c的补码均为“1111 1111”,但是在进行整型提升时,因为定义a、b、c的数据类型不一样,所以整型提升后的原码也不同,对于char和signed char来说,因为char类型默认为有符号型,所以整型提升后与signed char对应的原码相同,即“1000 0000 0000 0000 0000 0000 0000 0001”,打印结果为-1。
对于unsigned char 来说,整型提升时补充数字全为零,所以c的补码为“0000 0000 0000 0000 0000 0000 1111 1111”,因为其为正数,所以补码反码原码相同,打印结果为255。
练习2:
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
首先显然可以看出-128为负数,所以其原码、反码、补码不相同。写出-128的原码,即“1000 0000 0000 0000 0000 0000 1000 0000”,再由原码推出其补码为“1111 1111 1111 1111 1111 1111 1000 0000”,最后得出内存“a”中的八位数据为“1000 0000”。这一步是必须,无论是对于signed char 还是 unsigned char一样。
接着将“1000 0000”进行整型提升,得到补码为“1111 1111 1111 1111 1111 1111 1000 0000”(这个时候一定要注意,因为打印出的数据类型是无符号数,所以这时我们要把a作为正整数来看待,在将-128放入内存时是将它作为一个负数看待,在打印时又变成了一个正数,所以这里非常容易出错和混淆,需要特别注意)。
最后,打印结果为4294967168
练习3:
#include <stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
}
通过上面的两个练习,可以总结出一套流程来分析此类问题,我将此类问题的分析步骤总结为三步:
1、判断从哪里取?
2、判断正负数。
3、判断取出来放哪?
以此题为例:判断从哪里取?意思就是要看存储数据的空间是怎么定义的,是int 还是char int 或者是signed/unsigned char int,若是unsigned char直接补“0”,若是char或signed char看最高位是什么就补什么,int不需要补任何数据。
判断正负数,这一步非常容易出错,有些数本来就是正数,有些数是由unsigned char类型定义的,以上两类不需要进行原码反码补码转换,对其进行转换后得出的结果肯定是错误的。
判断取出来放哪?意思就是打印输出时,其类型是“%d”还是“%u”。若为“%u”,则不需要进行补码转换,直接将32位二进制转换成十进制就是输出结果;若为“%d”,则不但需要进行原码转换,还需要根据最高位来判断正负。
所以可以改变练习3代码中的输出数据类型来验证,若将输出类型改为“%d”,结果为-10;若将输出类型改为“%u”,结果为4294967286。
整型提升时的注意事项:
1、在对“c”进行整型提升时,要先将“a+b”的补码进行截断,再进行整型提升。
2、若“c”的补码不是“1000 0010”,而是“1 0000 0010”,此时要注意char c的最高位是“0”而不是“1”。
3、在得到数据的补码后,首先要判断其反码补码原码是否一致,也就是说判断其是否为正数,不要一得到补码就进行转换,很容易出错。
4、在练习2中,将-128放入内存时,是将它作为负数看待的,所以这个时候要转换它的原码、反码、补码,但是将它从内存中读取并打印时,是将它作为一个无符号数看待的,这个时候若仍然进行原码反码补码转换,会出现错误。