我们知道日常生活中使用的数分为整数和实数,整数的小数点固定在数的最右边,可以省略不写,而实数的小数点则不固定。在计算机中只能识别和表示“0”和“1”,而无法识别小数点,因此要想使得计算机能够处理日常使用的数据,小数点的问题是不可避免的。
关于计算机系统中实数的表示,在下篇文章中会讲解。本篇博客我们讲解的是整数在计算机系统中如何表示。
在各种大学教材,各种网站论坛中,对于整数编码表示方法的正确打开姿势(姿势要帅)如下:
1、机器数
机器数(computer number)是数字在计算机中的二进制表示形式。机器数有2个特点:
①、符号数字化。因为计算机硬件只认识两种物理状态(用0和1表示),因此数据的正负号在机器里就用一位二进制0或者1来区分。在计算机用一个数的最高位存放符号, 0代表符号“+”,以1代表符号“-”。
②、机器数的大小受机器字长的限制。机器内部设备一次能表示的二进制位数叫机器的字长,一台机器的字长是固定的。字长8位叫一个字节(Byte),机器字长一般都是字节的整数倍,如字长8位、16位、32位、64位。
比如在字长为8的计算机中,十进制数+5,其机器数为00000101;十进制数-5,其机器数为10000101。
2、真值
计算机机器数真正的值称为真值。因为机器数的最高位是符号位,所以我们在计算真值的时候要分区分开。
比如上面讲的机器数10000101,单纯作为一个二进制数,我们转换为十进制是133。但是其真值是不计算符号位的,其最高位的1表示"-"。所以10000101的真值为-5。
3、机器数的原码、反码、补码三种形式
前面我们讲过机器数是在计算机中的二进制表示形式,但是在计算机中,这种表现形式又分为原码、反码、补码等三种最常用的形式。
ps:下面举例都是字长为8。
①、原码
原码=符号位+真值
比如:
[+5]原码=0 0000101
[-5]原码=1 0000101
原码表示与真值对应直观,而且转换也简单。但是用原码进行加减运算的时候,会出现以下问题:
使用原码计算表达式:1 - 1 = 0 1 - 1 = 1 + (-1)= [00000001]原 + [10000001]原 = [10000010]原 = -2
注意:计算机是没有减法器,只有加法器,减法运算可以转换为加上那个数的负数。
我们发现通过原码计算1 - 1 表达式结果居然是 -2。所以早期计算机机器数采用原码编码的时候,在进行原码加减运算时,必须先判定是否是两个异号数相加或两个同号数相减,若是,则必须判定两个数的绝对值大小,根据判断结果决定运算结果符号,并用绝对值大的数减去绝对值小的数。也就是说用这样一种形式进行加运算时,负数的符号位不能与其数值部分一道参加运算,而必须利用单独的线路确定符号位。很显然,这样设计电路就很复杂,这是不经济实用的,为了解决这个问题,反码产生了。
②、反码
反码:正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外。
我们用反码来计算 1 - 1
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
看上去结果好像是正确的了,但是大家发现没,结果是-0,虽然对于0的符号没有什么实际意义。但是在计算机中,0如果用原码和反码表示会有两种形式:
[+0]=[0000 0000]原=[0000 0000]反 [-0]=[1000 0000]原=[1111 1111]反
两种编码就两种编码吧,只不过是多占用一个计算机表示数的编码形式。只要结果是正确的,我们还是能够忍受的,然而。。。。
请用反码计算表达式 2 -1
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反 = [0000 0000]反 = [0000 0000]原 = +0
是不是很奇怪,原码计算 2 - 1 得到的结果居然是 0 。其实稍微分析计算过程我们也知道,再用反码进行加法运算的时候发生了进位,而由于字长为8,进位就直接省略了,便造成了错误。这肯定是不被允许的,所以采用反码的计算机解决办法如下:
反码的符号位相加后,如果有进位出现,则要把它送回到最低位去相加(循环进位)。
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反+[0000 0001]循环进位 = [0000 0001]反 = [0000 0001]原 = +1
13 - 6 = 13 + (-6)= [0000 1101]原 + [1000 0110]原 = [0000 1101]反 +[1111 1001]反+[0000 0001]循环进位=[0000 0111]反 = [0000 0111]原 =+7
采用反码运算虽然较好的解决了原码运算所遇到的困难或问题,但由于循环进位需要二次算术相加,延长了计算时间,这同样给电路带来麻烦。这时候补码登场了。
③、补码
补码:正数的补码与原码相同,负数的补码等于其反码的末位加1
我们来看下面这个例子:
2 - 1 = 1
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反 = [0000 0010]补 + [1111 1111]补 = [0000 0001]补 = [0000 0001]原 =+1
9 + 12 = 21
9 + 12 = [0000 1001]原 + [0000 1100]原 = [0000 1001]补 + [0000 1100]补 = [0001 0101]补 = [0001 0101]原 = 21
我们发现补码运算就很简单了,产生的进位直接舍去,而且不做多余的操作也解决了进位的问题。还有 +0 和 -0 的表示,在原码和反码都有两种形式,但是补码却只有一种:
[+0]=[0000 0000]原=[0000 0000]反=[0000 0000]补 [-0]=[1000 0000]原=[1111 1111]反=[0000 0000]补
就这样我们完美的解决了计算机中整数运算的问题。计算机的机器数采用补码的形式,我们在做算术运算的时候,既不需要额外的判断,又能得到准确的结果。
看上去本文应该结束了,然而......
请求出 127+1 的值
4、溢出
接着上面抛出的问题,127+1的值,我们现在程序中看看:
public static void main(String[] args) { byte x = 127; byte y = 1; byte k = (byte) (x+y); System.out.println(k); //-128 System.out.println(Byte.MIN_VALUE+"~"+Byte.MAX_VALUE); //-128~127 }
byte在计算机正好是一个字节,也就是8位二进制序列。我们发现127+1结果不是128,反而是-128,这就是结果发生了溢出。因为byte表示数的范围是-128-127,128超出了这个范围。用补码计算如下:
1 + 127 = [0000 0001]原 + [0111 1111]原 = [0000 0001]补 + [0111 1111]补 = [1000 0000]补
我们发现这个数的符号位没有发生进位,但是数值最高位发生了进位。在看前面的2-1
2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]补 + [1111 1111]补 = [0000 0001]补 = [0000 0001]原 =+1
这个表达式符号位和数值最高位发生了进位,但是结果却是正确的。总结如下:
只有一个高位进位或者符号位进位就为溢出的规则。
溢出是每种编码在运算时都不可避免的,一般来讲结果超过字长所表示数的范围都会发生溢出。而判断机器是正常进位还是溢出的基本依据,在微型机中可用异或电路来实现上述的判断。在实际编码中解决办法也很简单,就是将结果用更大范围的编码形式接收即可。比如两个byte类型的数相加,我们用 int 来接收即可。
public static void main(String[] args) { byte x = 127; byte y = 1; int k = x+y; System.out.println(k); //128 System.out.println(Byte.MIN_VALUE+"~"+Byte.MAX_VALUE); //-128~127 }
所以我们可以说用补码进行运算,在不考虑溢出的情况下,结果都是正确的。确实也是这样,但是......
请求出 -128 的补码?
5、剧情反转
上面的给出的问题,-128 的补码,我们首先想到去求它的原码,嗯,原码应该是 [1000 0000],不对,第一位不是符号位吗,[1000 0000]应该表示 -0。那应该怎么用原码表示 -128呢,我们发现字长为 8 的计算机用原码是无法表示的,反码也是一样。我们看看补码,用 -127- 1 的表达式结果来计算 -128 的补码:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
-128的补码形式为 [1000 0000],我们能通过算术表达式得到某个数的补码形式,但是为什么直接就求不出来?那么计算机自己是怎么实现的呢?
再来看这样一个问题:我们日常使用的钟表,比如现在钟表指向的是 10点钟,我要将钟表调整到 6 点钟,则有两种拨法:
①、顺时针将时针拨动 8 格
②、逆时针将时针拨动 4 (12-8) 格
由此给大家普及一个概念叫 “模”,钟表便是一个典型的模运算系统,其模数为12。
同理,对于十进制两位数,在将结果百位舍掉的情况下,50可以用60-10得到,或者60+90得到。这里的90也就是100-10得来的,那么我们就说十进制两位数运算系统的模数为100。
我们判定:两个相加等于模的数互为补数。
在模表示的范围内做减法运算,可以将“X-Y”的减法变更为“X+Y的补数“的加法,当然这里不考虑结果溢出。
上面我们举的例子都是大数减小数,如果是小数减大数会怎样?
如果是10-80,结果应该是-70。但是如果按照 10+(100-80)的说法,结果是30。很明显,30和-70不是同一个结果,而且也没有产生百位进位。那我们应该怎么办呢?
解决办法很简单,就是让这两个数相等,而且这正好解决了负数的表示方法,-70的绝对值的补数正好是30。
但是问题又来了,这里的30已经表示正数30了,现在又表示负数-70,那我们怎么知道它到底表示哪个数?
为了解决这个问题,我们给这套规则规定一个范围,原来是0~99的正数,现在既然要用部分正数来代替负数了,那就要规定一个范围来使得一个数只代表一个含义,正好一人一半,0~49这个区间就代表正数,50~99的区间就用来代表各自补数的负值,例:98就代表-2
所以0-99的编码数可以表示的数的范围为 -50-49。
我们解决了十进制两位数的减法运算,那么在字长为 8 的计算机系统中,我们又该如何呢?
8位二进制数可以表示的数为2的8次方,0-255,一共 256 个数,0也要占用一位数。所以我们说 256 是8 位二进制数的模,这和上面说的十进制两位数0-99,模为100是一样的。
我们按照前面讲的逻辑,一半的数0-127,代表其正数本身,另一半的数128-255表示其补数的负值,即-1~-128。
所以而 “X-Y”的减法 就用 “X+Y的补数” 的加法来表示,即将减法的形式转换为加法的形式了,而且计算结果还是正确的。
注意:这里还是一样,不考虑结果的溢出,也就是计算值和结算结果都必须在-128~127之间,一旦超过这个范围,结果就不准了,这也是程序员日常编码说的int=int+int,如果结果大于int类型表示的范围,那得出来的结果肯定不准。
由此我们得出来的结论是:
计算机编码其实并没有什么所谓的符号位,但是由于计算机没有减法运算,为了将负数变为某个可以运算的编码来进行加法运算,补码产生了。这也间接说明了正数的补码是不变的,而负数的解决办法是首位不变,其余的取反再加1。
我们上面说的补码怎么得来的,就是 模-绝对值 。
所以这个时候我说让你求 -128的补码,你马上就会知
256 - |-128|=128 而128的二进制补码是不是就是 [1 0 0 0 0 0 0 0]
让你求 -1 的补码,你马上就会知
256 - |-1| = 255,其255的二进制补码形式就是[1111 1111]
注意:关于这样求补码的具体数学证明,请参考《计算机组成与系统结构》。
6、总结
本篇文章你可以直接从第 5 点开始看,忘掉计算机编码的什么首位符号位,忘掉计算机补码是由原码取反加1,回归简单直白的理解。计算机是机器,硬件能理解的只有高低电平,也就是0或者1。它知道什么是符号位吗?这些规则只不过是为了更好的完成减法运算所yy出来的。
大学也学过这些编码方式,但是都是背书式记忆,希望这篇文章能给大家带来一些帮助。
个人见解,如有错误欢迎大家抛砖!!!
参考书籍:《计算机组成与系统结构》
参考文章: https://www.zhihu.com/question/20458542?sort=created
来源:https://www.cnblogs.com/ysocean/p/7826374.html