原码 反码 补码 移码

众所周知,java中两个浮点数判断相等,不可以直接使用 == ,而浮点数的运算,有时结果也令人大吃一惊。如 System.out.println(100.1F == 100.099999999F) 结果true,System.out.println(0.05 + 0.01) 结果为 0.060000000000000005,System.out.println(0.51F * 0.51F) 结果为0.26009998。这一切是如此的不符合数学常识,但在了解了计算机数字的存储方式后又会觉得是如此自然。

记号说明
下标10 2 分别表示十进制,二进制的数字;
下标 原 反 补 移 表示此数字为二进制的原码,反码,补码,移码表示。

十进制和二进制的转化

整数十进制二进制互转

二进制 => 十进制 \(\pm a_{n}a_{n-1}...a_{1}a_{0}\) = \(\pm a_{n} \times 2^{n} + a_{n-1} \times 2^{n-1} \times ... \times a_{1} \times 2^1 \times a_0 \times 2^0\)
\(\pm (01100100)_2\) = \(\pm (0 \times 2^0 + 0 \times 2^1 + 1 \times 2^2 + 1 \times 2^5 + 1 \times 2^6)\) =\(\pm (100)_{10}\)
十进制整数 => 二进制 有二个方法:

机算法: "辗转除2,直到商为0,逆序取余"。
\((17)_{10}\) = \((10001)_2\) 的步骤如下:

\[17 \div 2 = 8 ...1 \]

\[8 \div 2 = 4 ... 0 \]

\[4 \div 2 = 2 ... 0 \]

\[2 \div 2 = 1 ... 0 \]

\[1 \div 2 = 0 ... 1 \]

逆序取余为\((10001)_2\)即为十进制17的二进制。

心算法: 熟悉二进制的同学,可以直接找个一个十进制数字比较接近的二进制,然后通过加减拼凑,得出二进制。
如 513 我们先找个512的二进制 可以很容易得到为\((10-0000-0000)_2\) 立即得到\((513)_{10} = (10-0000-0001)_2\)
这种方法对于我们学计算机的同学在数字电路 模拟电路 微机原理 离散数学中大量用到。一般地,在\(\pm\) 8092以内我们都可以快速拼凑出。

最后,在不考虑超过的存储字节数时,十进制整数与二进制是一一对应的(满射)。这也解释了,对于java中int 型 我们为什么可以使用 == 来 比较两个整数,因为一个在int不溢出的情况下,一个整数对应唯一的二进制,那么在计算机底层整数的存储中,我们比较二个整数对应的二进制的所有位数,若全相等,两个整数也必然相等。而小数则不一定了。

小数十进制二进制互转

小学我们就知道,小数分为有理小数和无理小数,有理数包括有限小数和无限循环小数,有限小数和无限循环小数一定可以用分数表示,而无限不循环小数,无法用分数表示。
比如 十进制的 \(0.3 = \frac{3}{10}\) \(0.\dot{3} = \frac{1}{3}\)有上面的整数和二进制的关系,我们不妨可以推断,在不考虑存储字节数时,有理小数和二进制可以一一对应,无理数显然只能近似存储。这是十进制小数用二进制表示的一个大的原则。即,只要位数(存储字节足够大)给的多,每一个有理小数一定可以对应一个唯一的二进制,无理数只能近似存储,那么无理数的二进制就有可能和一个有理数的二进制一样以上只是我的一些论断,真实计算机可能处理的都是有限小数(无限循环小数也会按需取长度)。然而,存储空间是宝贵的,计算机给一个小数的存储字节一般为4字节(32bit)或 8字节(64bit)。在这样设计的浮点系统中,两个不同整数若不溢出,他们对应的二进制一定是不相同的,两个相等的整数,对应的二进制一定是相等的。然而,小数就不一定了,二两不同的小数若不溢出,他们对应的二进制可能相同(极为相近时,一般就相同)也可能不同。而 == 运算符就是比较两数的二进制位数,若位数全相等,则认为两数相等,否则,认为不相等, 这应用在整数上毫无问题,应用在小数上可能会出现都多问题,如上述100.1F,100.099999999F显然不等,但在计算机中二进制的每位完全一样,故返回true.

二进制 => 十进制 \(\pm (0.a_1a_{2}...a_{n-1}a_n)_{2}\) = \(\pm (a_1 \times 2^{-1} + a_2 \times 2^{-2} + ... + a_{n-1} \times 2^{-(n-1)} + a_n \times 2^{-n})_{10}\)
十进制 => 二进制 采用每次用结果(原数据)小数部分$ \times 2$ 取结果的整数部分,直到小数部分为0;或者当小数部分永远不可能为0时,我们取合适的整数部分,近似表示该十进制小数。
通过3个例子:
例1: \((0.125)_{10}\) 怎么转成二进制小数?
\(0.125 \times 2 = 0.25\) 取0,\(0.25 \times 2 = 0.5\) 取0,\(0,5 \times 2 = 1.0\) 小数部分为0,结束转化,取1。 故,\((0.125)_{10} = (0.001)_{2}\)
例2 :\((0.15625)_{10}\) 怎么转成二进制小数?
$0.15625 \times 2 =0.3125 $取0,\(0.3125 \times 2 = 0.625\) 取0,\(0.625 \times 2 = 1.25\) 取1,\(0.25 \times 2 = 0.5\) 取0,$0.5 \times 2 = 1.0 $取1。小数部分为0,结束转化,故 \((0.15625)_{10} = (0.00101)_{2}\)
例3 :\((0.15)_{10}\)怎么转成二进制小数?
\(0.15 \times 2 = 0.3\) 取0,\(0.3 \times 2 = 0.6\) 取0,\(0.6 \times 2 = 1.2\) 取1,\(0.2 \times 2 = 0.4\) 取0,\(0.4 \times 2 = 0.8\) 取0,\(0.8 \times 2 =1.6\) 取1,\(0.6 \times 2 = 1.2\) 取1,注意到进入到了一个循环之中,小数部分永远都不会为0,所以我们根据精度需求,取合适的长度,取得越长,精度越高。故,\((0.15)_{10} = (0.0010011001)_{2}\) 现在我们将\((0.0010011001)_{2}\)还原成十进制小数为\((0.1494140625)_{10}\)
根据例三,试想若我们只用11(1位符号位+10为有效位)为来存储浮点数,那么 \((0.1494140625)_{10}\)\((0.15)_{10}\) 在我们设计的存储系统中二进制每一位都一样,我们利用 == 运算符比较这两个数字时,就是得到true。至此,我们已经基本揭开计算机系统中,有限小数无法精确存取的原因:在存储位数的限制下,无法完全转成二进制,或者,十进制小数本身就无法完全转成而二进制,其二进制存储的就是该十进制小数的近似数。 另一方面,我们也完全理解了浮点数的比较中,显然的不等,却 经过 == 运算相等,是由于浮点的二进制完全一样,故返回true。以上,都是我们浅谈浮点数在计算机内存中的存储。

原、反、补、移码

以上,我们在将将十进制转成二进制的时候,保持了+ - 符号,如 二进制数字\((+0011)_2\),这种记法,我们称为真值,而机器存储时会将 + 用 0,-用 1表示,这种表示称为机器码,如\((0,0011)_{2}\)。下面简单介绍一下真值的原码,反码,补码,移码表示形式。(为什么要引入这些真值二进制的表示形式呢?主要是为了计算机运算的方便!

原码表示形式

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示真值。以每个数字以8位二进制存储(1位存符号)为例:
\([+0000011]_{2} = [0,0000001]_{原}\), \([-0100010]_{2} = [1,0100010]_{原}\)\([+0.0110011]_{2} = [0.0110011]_{原}\) , \([-0.0010001]_{2} = [1.0010001]_{原}\)
特别地,\([+0]_{10} = [+0000000]_{2} = [0,0000000]_{原}\),\([-0]_{10} =[-0000000]_{2}=[1,0000000]_{原}\)

细心的读者会发现,前两个例子是整数 我们用了 \(,\) ,后面两个例子是小数,我们用了 \(.\), 那么问题来了?计算机中不会存储 \(.\) \(,\) 如计算机中存了\([100000001\) ,我怎么知道这个数字是十进制的整数 \(1,0000001\) 还是 \(1.0000001\),其实计算机中存储的数字并不是下面我们谈到的原码 补码 移码表示,而是采用IEEE754标准来表示数字。

反码表示形式

正数的反码是其本身,负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。以每个数字以8位二进制存储为例:
\([+0001001]_{2} = [0,0001001]_{原} = [0,0001001]_{反}\)\([-0001101]_{2} = [1,0001101]_{原} = [1,1110010]_{反}\)
\([+0.0000101]_{2} = [0.0000101]_{原} = [0.0000101]_{反}\)\([-0.1110000]_{2} = [1.1110000]_{原} =[1.0001111]_{反}\)
特别地,$[+0]{10} = [+0000000] = [0,0000000]{原} = [0,0000000] \(,\)[-0]{10} =[-0000000]=[1,0000000]{原} =[1,1111111]$
值得一提的是,反码是引入补码的中间产物。

补码表示形式

正数的补码和其反码一致,负数的补为反码加1。以每个数字以8位二进制存储为例:
\([+0001001]_{2} = [0,0001001]_{原} = [0,0001001]_{反} = [0,0001001]_{补}\)\([-0001101]_{2} = [1,0001101]_{原} = [1,1110010]_{反} = [1,1110011]_{补}\)
\([+0.0000101]_{2} = [0.0000101]_{原} = [0.0000101]_{反} = [0.0000101]_{补}\)\([-0.1110000]_{2} = [1.1110000]_{原} =[1.0001111]_{反} = [1.0010000]_{补}\)
特别地,\([+0]_{10} = [+0000000]_{2} = [0,0000000]_{原} = [0,0000000]_{反} = [0,0000000]_{补}\) ,
\([-0]_{10} =[-0000000]_{2}=[1,0000000]_{原} =[1,1111111]_{反} = [0,0000000]_{补}\)
注意到 +0 -0 的补码相等。

移码表示形式

将补码的符号位0,1颠倒,即为移码。。以每个数字以8位二进制存储为例:
\([+0001001]_{2} = [0,0001001]_{原} = [0,0001001]_{反} = [0,0001001]_{补} = [1,0001001]_{移}\)
\([-0001101]_{2} = [1,0001101]_{原} = [1,1110010]_{反} = [1,1110011]_{补} = [0,1110011]_{移}\)
\([+0.0000101]_{2} = [0.0000101]_{原} = [0.0000101]_{反} = [0.0000101]_{补} = [1,0000101]_{移}\)
\([-0.1110000]_{2} = [1.1110000]_{原} = [1.0001111]_{反} = [1.0010000]_{补} = [0,0010000]_{移}\)
特别地:
\([+0]_{10} = [+0000000]_{2} = [0,0000000]_{原} = [0,0000000]_{反} = [0,0000000]_{补} = [1,0000000]_{移}\)
\([-0]_{10} = [-0000000]_{2} = [1,0000000]_{原} = [1,1111111]_{反} = [0,0000000]_{补} = [1,0000000]_{移}\)

为何引入码反码,补码,移

反码是引入补码的副产物,补码的引入统一了二进制的加减:
首先我们看用原码来计算二进制的加减问题,当两数符号位相同时,不考虑数值溢出时,做加法,保持符号位不变,其他位置直接加就ok,但是当两数符号位不同时,我们需要用绝对值大的数减去绝对值小的数,符号位取绝对值大的数,总之加法和减法不能统一用加法做。举个例子:
直接按加法做的话:
$(-3)+{10} + (+1){10} = (1,0000011) + (0,0000001){原} $ 先取结果的符号位为-3的符号位1,再二进制\((0,0000011)_{原} - (0,0000001)_{原} = (0,00000010)_{原}\) 故结果为= \((1,00000010) = (-2)_{10}\),其实有点类型c++中的重载了+运算符。若”正常“算的话 \((-3)+{10} + (1)_{10} = (1,0000011)_{原} + (0,0000001)_{原} = (1,0000100)_{原} = (-4)_{10}\)
引入的补码解决了原码运算过程加减不能统一的问题,仍然以 \((-3)_{10} + (+1)_{10}\)为例:
\((-3)_{10} = (1,0000011)_{原} = (1,1111100)_{反} = (1,1111101)_{补})\)
\((+1)_{10} = (0,0000001)_{原} = (0,0000001)_{反} = (0,0000001)_{补})\)
\((-3)+{10} + (1)_{10} = (1,1111101)_{补} + (0,0000001)_{补} = (1,1111110)_{补} = (1,1111101)_{反} = (1,0000010)_{原} = (-2)_{10}\)
总之,补码统一了计算机中数字计算的加减问题,全统一成加法。
而移码解决了计算机中数字比较的问题。举个例子:
\((+21)_{10} = (0,0010101)_{原} = (0,0010101)_{反} = (0,0010101)_{补} = (1,0010101)_{移}\)
\((-21)_{10} = (1,0010101)_{原} = (1,1101010)_{反} = (1,1101011)_{补} = (0,1101011)_{移}\)
\((+31)_{10} = (0,0011111)_{原} = (0,0011111)_{反} = (0,0011111)_{补} = (1,0011111)_{移}\)
\((-31)_{10} = (1,0011111)_{原} = (1,1100000)_{反} = (1,1100001)_{补} = (0,1100001)_{移}\)
\((+8)_{10} = (0,0010000)_{原} = (0,0010000)_{反} = (0,0010000)_{补} = (1,0010000)_{移}\)
\((-8)_{10} = (1,0010000)_{原} = (1,1101111)_{反} = (1,1110000)_{补} = (0,1110000)_{移}\)
观察上述的原码,我们可以看出
\((1,0010000)_{原} < (1,0010101)_{原} < (1,0011111)_{原}\)\(-8 < -21 < -31\) 其实恰恰相反;
\((0,0010000)_{原} < (0,0010101)_{原} < (0,0011111)_{原}\)\(8 < 21 < 31\) 是对的。
\((0,0010000)_{原} < (0,0010101)_{原} < (0,0011111)_{原} < (1,0010000)_{原} < (1,0010101)_{原} < (1,0011111)_{原}\)\(8 < 21 <31 < -8 < -21 <-31\)
综上,不难得出原码,利用原码比较时,不可根据统一比较规则直接得出大小顺序。
观察上述的补码,我们可以看出: $ -31 <-21<-8 $ , $ 8 < 21 < 31$ 然而正负的比较确实恰好相反,但比纠正了原码负数比较恰好逆序。但正负混合仍然需要区分。
最好,观察上述的移码,我们可以直接得出:
$(0,1100001)
< (0,1101011){移} < (0,1110000) < (1,0010000){移} < (1,0010101) < (1,0011111)_{移} $
即为 : \(-31 < -21 < -8 < 8 < 21 <31\) 可以正负一次性比较
综上,我们引入补码解决了二进制的加减统一为加法,引入移码解决了二进制的大小比较问题

to do list:
IEEE754标准介绍

posted @ 2020-06-27 00:18  ahpuched  阅读(619)  评论(0编辑  收藏  举报