前言
int a=12;
int b=1.234;
问:a 和 b 在计算机中到底是如何存储的?
答:转为二进制?
问:转成二进制就直接存储了?
答:...
问:小数 b 如何转成二进制的?
答:...
我们日常更多的时候都在使用十进制的数,要知道我们的祖先可厉害了,应该很长一段时间都在使用 16 进制。譬如一个成语叫“半斤八两”,解释:半斤、八两轻重相等,比喻彼此不相上下。等等!半斤和八两怎么相等,对的,宋代一斤就是等于十六两,当然半斤等于八两。
都知道计算机使用的是二进制,只用 0 和 1 就能表示数字,但是计算机到底是如何解决数字存储的问题?下面分别讨论整数和小数的存储:
如何存储整数
假设我们现在有一个 8 位的操作系统,最高位是符号位,1 表示负数,0 表示正数,所以能表示的最大的整数区间为 11111111 ~ 01111111 => -127 ~ 127。
但是在存储表达的时候会遇到一个问题,10000000 和 00000000 两个 0 的问题,+0 和 -0 应该也是相等的,这给电路设计上带来了很多麻烦和多余的计算规则。
十进制转成二进制
简单点的记忆就是:除 2 取余再倒序。
- 12 / 2 = 6..0
- 6 / 2 = 3..0
- 3 / 2 = 1..1
- 1 / 2 = 0..1
0011 倒序 即为 1100,转成八位即在前面补0,则结果为 0000_1100。
0000_1100 是 java8 中的写法,我觉得看起来比较舒服且前后连贯,_ 是为了看起来比较清楚人为添加的,后面沿用这种写法,0000_1100 = 00001100= 0000 1100
都是相等。
一个天才的设计师
看了很多的文档没有找到解决这个问题的作者是谁?不管是谁反正提出了补码
这个概念,真的是有效的解决了这个问题,不仅解决了 0 这个问题还带来了一个更大的优点,后面补充。
正数的原码、反码、补码
正数原码 = 反码 = 补码
- 12 的原码是 0000_1100
- 12 的反码是 0000_1100
- 12 的补码是 0000_1100
这里并没有废话,尽是科学。
负数的原码、反码、补码
反码 = 原码符号位不变其他位取反
补码 = 反码 + 1
- -12 的原码是 1000_1100
- -12 的反码是 1111_0011
- -12 的补码是 1111_0100
0 的存储
+0
- 原码:0000_0000
- 反码:0000_0000
- 补码:0000_0000
-0
- 原码:1000_0000
- 反码:1111_1111
- 补码:0000_0000
这里应该把前面的问题都解决了吧,补码不仅解决了 +0 和 -0 的问题,还神奇的把符号位都去掉了,计算机在运行的过程中从而可以更简单的进行运算。
减法运算
为了效率,计算机底层计算的时候是没有减法运算,减法运算都转成加法运算。
12 - 12 => 12 + (-12) => 0000_1100 + 1111_0100 => 0000_0000
- 0000_110
0
+ 1111_0100
= 0 + 0 = 0 - 0000_11
0
0 + 1111_010
0 = 0 + 0 = 0 - 0000_1
1
00 + 1111_01
00 = 1 + 1 = 0 进 1 - 0000_
1
100 + 1111_0
100 = 1+ 0 + 进1= 0 进 1 - 000
0
_1100 + 1111
_0100 = 0+ 1 + 进1= 0 进 1 - 00
0
0_1100 + 111
1_0100 = 0+ 1 + 进1= 0 进 1 - 0
0
00_1100 + 11
11_0100 = 0+ 1 + 进1= 0 进 1 -
0
000_1100 +1
111_0100 = 0+ 1 + 进1= 0 进 1
因为只能存储 8 位,最后一个进 1 爆掉就剩下 0000_0000 了。
这里都是以 8 位计算机进行举例,现在的 32 位或者 64 位计算机都是同理的。
如何存储浮点数
相信很多人小数转成二进制都是不知道如何运算的,更别谈存储了,下面我就娓娓道来。
文章刚开始编辑的时候将浮点数称为小数,由于发现这样不专业,其实小数不一定都是浮点数,在那个没有标准各自为政的早计算机时代其实还是有定点数,感兴趣可以看参考文档。
浮点数转成二进制
1.234 在计算机中转成二进制是按照是分整数部分和小数部分进行的,整数部分上面以前讲到,这里说下小数部分 0.234 为例:
简单点的记忆就是:乘 2 取整再顺序。
- 0.234 * 2 = 0.468 => 整数部分为 0 => 取 0
- 0.468 * 2 = 0.936 => 整数部分为 0 => 取 0
- 0.936 * 2 = 1.872 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.872 * 2 = 1.744 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.744 * 2 = 1.488 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.488 * 2 = 0.976 => 整数部分为 0 => 取 0
- 0.976 * 2 = 1.952 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.952 * 2 = 1.904 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.904 * 2 = 1.808 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.808 * 2 = 1.616 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.616 * 2 = 1.232 => 整数部分为 1 => 取 1 (并将整数部分抹去)
- 0.232 * 2 = 0.464 => 整数部分为 0 => 取 0
- ...
下面就不写下去了结果是 0.001110111110...,写的越长精度越高。
可见浮点数存储必将是一个头疼的问题,浮点数底层的逻辑肯定也是比整数更为复杂。
IEEE754 标准
电气电子工程师学会(英语:Institute of Electrical and Electronics Engineers)简称为 IEEE,IEEE754是专门规定浮点数该如何存储的一个标准,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。
任意一个浮点数都可以表示为:
(-1)^s 表示符号位,当 s=0,V 为正数;当 s=1,V 为负数。
M 表示有效数字,大于等于 1,小于 2。
2^E 表示指数位。
例子:V = 0.234(十进制) = 0.001110111110(二进制) = 1.110111110 * 2^-3 ,则 s=0 ,M= 1.110111110,E=-3。
IEEE754 规定:
对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。
对于 64 位的浮点数,最高的 1 位是符号位 s,接着的 11 位是指数 E,剩下的 52 位为有效数字 M。
IEEE754 还有一些特殊的规定:
针对 M
由于 1<= M <=2 ,所以 M 始终为 1.xxxx 形式,xxxx 表示小数,那些对于计算机底层严苛的设计师这时候又要将 1 这一位舍掉,只保留 xxxx 部分,这样的好处是可以多储存一位有效数字。
针对 E
由于 E 是一个 8 位的无符号存储,只能表示 0 ~ 255,现实中的指数还存在负数,所以规定:E必须再加上一个中间数,对于 8 位的 E,这个中间数是 127;对于 11 位的 E,这个中间数是 1023,这样就可以将指数的表达范围扩大到 -127 ~ +128
和 -1023 ~ +1024
。
实际中的取值范围是 -126~+127,-127 和 128 被用作特殊值处理,双精度同理。
阮一峰的博客中写的是 减去一个中间数 应该是有误的。
举个例子,如果 E=17,则 17+127 =144 ,实际的存储指为 144。
针对 E 还有一些特殊值的情况
- 如果指数 E 是 0 并且尾数的小数部分是 0,这个数是 ±0(和符号位相关)。
- 如果指数 E 是 1 并且尾数的小数部分是0,这个数是±∞(同样和符号位相关)
- 如果指数 E 是 1 并且尾数的小数部分非0,这个数表示为不是一个数(NaN)。
计算
V = 0.234(十进制) = 0.00111011111001110110110010(二进制) = 1.11011111001110110110010 * 2^-3 ,则 s=0 ,M= 1.110111110,E=-3。
根据 IEEE754 标准转化 :
以 单精度为例
s 不变
E=-3+127=124(十进制) = 0111_1100(二进制)
M=110_1111_1001_1101_1011_0010
结果将他们连接 0_0111_1100_110_1111_1001_1101_1011_0010(s_E_M)
可以在线校验结果
小结
这篇文章详细介绍了计算机如何存储整数以及浮点数,个人也是从朦胧状态到理解透彻,参考了众多大佬的文章,在此表示感谢。有问题的朋友可以通过邮箱联系到我 jake.zou.me@gmail.com
。
计算机之美妙不可言啊!