二进制究竟如何表示整数和浮点数

整数篇

我们都知道计算机只能识别二进制,大学里我们都学过二进制和十进制之间的转换,下面用几个例子来解释(以下大部分场景用Java代码演示,使用4个字节共32位的int
由于Java中的int是有符号的,所以32位不能全部用来表示数字,得用最高位来表示正数还是负数(1是负数,0是正数),剩下的31位用来表示数字。

正整数

十进制->二进制

我们用43这个数字举例(原谅我没找到markdown中的语法,只能用截图代替)

image.png

高位都用0补齐

正
0 0000000 00000000 00000000 00101011

二进制->十进制

正
0 0000000 00000000 00000000 00101011

从右往左
1*2^0+1*2^1+0*2^2+1*2^3+0*2^4+1*2^5=43
1\ \ \ \ \ \ \ \ +2\ \ \ \ \ \ \ \ +0\ \ \ \ \ \ \ +8\ \ \ \ \ \ \ +0\ \ \ \ \ \ \ \ +32\ \ \ \ \ \ =43

我们在用工具校验下,没毛病!


image.png

负整数

十进制->二进制

负整数我们使用-11举例,首先符号位是1,然后11的二进制数表示是00001011,所以完整的是

负
1 0000000 00000000 00000000 00001011

-11是上面那串数字吗?错!
这里需要引入另一个概念,原码反码补码,计算机中都是以补码的形式进行存储的。
那为什么正整数的时候不说这个概念呢?
因为正整数原码=反码=补码三个都是相等的
但是负整数是不一样的,上面那一串其实是-11原码
原码反码的过程是:
符号位不变,其余所有位取反

原码:1 0000000 00000000 00000000 00001011
反码:1 1111111 11111111 11111111 11110100

反码补码的过程是:
符号位不变,直接+1

反码:1 1111111 11111111 11111111 11110100
补码:1 1111111 11111111 11111111 11110101
// 11111111111111111111111111110101 和上面计算的结果是一样的
System.out.println(Integer.toBinaryString(-11));

有了补码,想得到原码,有两种方法
第一种:只需要将上面的操作反过来就行
补码反码的过程是:
符号位不变,直接-1

补码:1 1111111 11111111 11111111 11110101
反码:1 1111111 11111111 11111111 11110100

反码原码的过程是:
符号位不变,其余所有位取反

反码:1 1111111 11111111 11111111 11110100
原码:1 0000000 00000000 00000000 00001011

第二种:用原码到补码的过程再做一遍也行
补码反码的过程是:
符号位不变,其余所有位取反

补码:1 1111111 11111111 11111111 11110101
反码:1 0000000 00000000 00000000 00001010

反码原码的过程是:
符号位不变,直接+1

反码:1 0000000 00000000 00000000 00001010
原码:1 0000000 00000000 00000000 00001011

是不是很神奇呢

二进制->十进制

先得把存储在计算机中得补码转换成原码,再将原码转换成十进制

补码:1 1111111 11111111 11111111 11110101
原码:1 0000000 00000000 00000000 00001011

然后符号位单独看,1011就是十进制的11,就能得到-11

负
1 0000000 00000000 00000000 00001011

溢出

在看浮点数之前,先看看整数溢出的情况
代码如下:

int i = Integer.MAX_VALUE;
// 2147483647
System.out.println(i);
int j = i + 33;
// -2147483616
System.out.println(j);

变量i已经是int的最大值了,如果在增加33,会得到什么呢?答案很明显,原本的正数变成了负数
下面我们用二进制的视角看下计算过程

# i
正
0 1111111 11111111 11111111 11111111
# 33 换成二进制
正
0 0000000 00000000 00000000 00100001

将两个数相加

  0 1111111 11111111 11111111 11111111
+ 0 0000000 00000000 00000000 00100001
————————————————————————————————————————
  1 0000000 00000000 00000000 00100000

我们前面说过了最高位是符号位,所以相加后的结果,计算机会识别为负数,而且是补码,所以要知道这串二进制代表的是哪个负数,需要把补码转成原码,用上面提到的两个方法中的一个就行

补码:1 0000000 00000000 00000000 00100000
反码:1 1111111 11111111 11111111 11011111
原码:1 1111111 11111111 11111111 11100000

最终的结果,不看符号位代表的十进制数是2147483616,加上符号就是-2147483616,和代码结果是一致的。
用工具验证下

image.png

再举一个例子,对int最小值做减法

int i = Integer.MIN_VALUE;
// -2147483648
System.out.println(i);
int j = i - 33;
// 2147483615
System.out.println(j);

和预期一样,负数变成了正数
下面我们用二进制的视角看下计算过程

# i
负
1 0000000 00000000 00000000 00000000
# 33 换成二进制
正
0 0000000 00000000 00000000 00100001

将两个数相减

  1 0000000 00000000 00000000 00000000
- 0 0000000 00000000 00000000 00100001
————————————————————————————————————————
  0 1111111 11111111 11111111 11011111

符号位是0,这就是一个正数,所以直接将这个转成十进制数是2147483615,和代码结果是一致的。
用工具验证下

image.png

浮点数篇

在进入浮点数讨论前,先请大家思考一个问题,由于之前整数部分是以int作为示例的,所以浮点数也同样以float作为示例,因为在Java中都是4个字节的。
来自百度百科对int的解释

int占用4字节,32比特,数据范围为-2147483648到2147483647

来自百度百科对float的解释

float占用4字节,32比特,数据范围为-3.4E+38 ~ 3.4E+38

那么问题来了,同样是4个字节的,只有0和1,一共32位,能表示的数字最多就是2^{32}个数字,为什么floatint表示的范围要大?
首先先了解下float的32个字节是如何分配使用的

符号位(S):1bit 指数位(E):8bit 尾数位(M):23bit
  • 符号位S:和int一样,0表示正数,1表示负数
  • 指数位E:8位的二进制能表示的范围是0~255
  • 尾数位M:实际存储的小数

然后还需要了解一个公式(IEEE 754)
value = (−1)^S∗2^{E−127}∗M
公式分为3部分

  • 第一部分:(−1)^S很好理解,用一个系数1或者-1去控制整个数的正负
  • 第二部分:2^{E−127},刚刚说了E的取值是0~255,所以这个部分的范围就是2^{-127}2^{128}
  • 第三部分:最复杂的就是这个M了,如果使用二进制表示浮点数,可以是1010.0101做一点转换1010.0101*1再做一点转换1010.0101*2^0,但是计算机在将该数存储之前会做一个规格化规格化指的就是把小数点都移动到第一位和第二位之间,像这样1.0100101但是之后的指数就要修改来保证数字大小并没有发生改变1.0100101*2^3规格化的好处就是在于不用去记录小数点的位置。这里2的指数被记做ee=E-127,例子中e=3,所以E=130规格化后必须保证整数部分是1,也就是1.xxxxxxxxx
    但是实际的情况比这个又要再复杂一点点
含义 符号位(S):1bit 指数位(E):8bit 尾数位(M):23bit
规格化后整数部分是1 0:正,1:负 0<E<255 规格化后的小数部分
规格化后整数部分是0 0:正,1:负 0 规格化后的小数部分
无穷大 0:正,1:负 255 0
NaN 0:正,1:负 255 不等于0

所以纠正下之前的一个关于E的结论,由于设计上E等于0或者255,整个float是算做特殊情况的,所以不算特殊情况的话E的取值是1~254,所以这个部分的范围就是2^{-126}2^{127}

举例说明:

float f = 33.25F;
int bits = Float.floatToIntBits(f);
// 0 10000100 00001010000000000000000
System.out.println(Integer.toBinaryString(bits));

首先将该数转换成二进制,那么如何把一个浮点数转成二进制呢?
我们先把一个浮点数,拆分成整数部分小数部分
整数部分33转换和之前介绍的一样,二进制表示为100001
小数部分0.25转换则略微有些不一样
小数部分不停的乘以2,取乘积结果的整数部分作为二进制数位,直至乘积结果小数部分消失。
例如:

0 | 0.25 * 2 = 0.5
1 | 0.5 * 2 = 1

所以,小数部分0.25,二进制表示为01
整合起来就是100001.01
这里需要提一嘴的是,不是所有小数都能在有限位数中被精确的表示成二进制数的,绝大部分小数都不行,所以这就是计算机无法精确表示小数的最主要原因。

// 33.25
100001.01
100001.01 * 1
100001.01 * 2^0
// 规格化,这里的 e=5
1.0000101 * 2^5
// E = e + 127
E = 5 + 127 = 132
// 132转换成二进制数就是 指数位的二进制表示
10000100
// M部分就是,规格化后的小数部分+右边全部补零至23位
00001010000000000000000
// 把整个结果拼起来,和java代码展示的结果是一致的
符号位  指数位        尾数位
0      10000100     00001010000000000000000

举例说明2:

float f = 0.25F;
int bits = Float.floatToIntBits(f);
// 0 01111101 00000000000000000000000
System.out.println(Integer.toBinaryString(bits));

首先同样将该数转换成二进制

// 0.25
0.01
0.01 * 1
0.01 * 2^0
// 规格化,这里的 e=-2
1.0 * 2^-2
// E = e + 127
E = -2 + 127 = 125
// 125转换成二进制数就是 指数位的二进制表示
01111101
// M部分就是,规格化后的小数部分+右边全部补零至23位
00000000000000000000000
// 把整个结果拼起来,和java代码展示的结果是一致的
符号位  指数位        尾数位
0      01111101     00000000000000000000000

举例说明3:

float f = -17.125F;
int bits = Float.floatToIntBits(f);
// 1 10000011 00010010000000000000000
System.out.println(Integer.toBinaryString(bits));
// -17.125
10001.001
10001.001 * 1
10001.001 * 2^0
// 规格化,这里的 e=4
1.0001001 * 2^4
// E = e + 127
E = 4 + 127 = 131
//  131转换成二进制数就是 指数位的二进制表示
10000011
// M部分就是,规格化后的小数部分+右边全部补零至23位
00010010000000000000000
// 把整个结果拼起来,和java代码展示的结果是一致的
符号位  指数位        尾数位
1      10000011     00010010000000000000000

所以回到开头的问题,为什么同样是4个字节32位,floatint能表示的数字范围要大很多。
int能表示的数字范围中的每一个整数,它都能正确的用二进制表示出来。
float则不能,哪怕抛开浮点数不看,让float只表示整数的话,在它的范围中也有许多数字它是
无法精确表示的,更不要说加了小数位后了。
同样的解释,也能用于longdouble
最后,再抛出一个问题:为什么BigDecimal能‘精确’的表示小数呢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容