小数 二进制 的 表示与转换
1. 小数用二进制如何表示
首先,给出一个任意实数,整数部分用普通的二进制便可以表示,这里只说小数部分如何表示
例如0.6
文字描述该过程如下:将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位;以此类推,直到小数部分为0。 特殊情况:小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中表示小数会出现误差的原因
下面我们具体计算一下0.6的小数表示过程
0.6 * 2 = 1.2 ——————- 1
0.2 * 2 = 0.4 ——————- 0
0.4 * 2 = 0.8 ——————- 0
0.8 * 2 = 1.6 ——————- 1
0.6 * 2 = 1.2 ——————- 1
我们可以发现在该计算中已经出现了循环,0.6用二进制表示为 0.1001 1001 1001 1001 …… 如果是10.6,那个10.6的完整二进制表示为 1010.100110011001……
##2. 二进制表示的小数如何转换为十进制 其实这个问题很简单,我们再拿0.6的二进制表示举例:0.1001 1001 1001 1001 文字描述:从左到右,v[ i ] × 2^(-i)^,i 为从左到右的index,v[ i ]为该位的值,直接看例子,很直接的
| 0. | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | .... | (第n位) |
|---|---|---|---|---|---|---|---|---|---|---|
| 2^-1^ | 2^-2^ | 2^-3^ | 2^-4^ | 2^-5^ | 2^-6^ | 2^-7^ | 2^-8^ | .... | 2^-n^ |
0.6 = 1 * 2^-1 + 0 * 2^-2 + 0 * 2^-3 + 1 * 2^-4 + ……
定点数
什么是定点数
数字既包括整数,又包括小数,而小数的精度范围要比整数大得多,所以如果我们想在计算机中,既能表示整数,也能表示小数,关键就在于这个小数点如何表示? 于是人们想出一种方法,即约定计算机中小数点的位置,且这个位置固定不变,小数点前、后的数字,分别用二进制表示,然后组合起来就可以把这个数字在计算机中存储起来,这种表示方式叫做「定点」表示法,用这种方法表示的数字叫做「定点数」。
定点数的数字表示方法
定点数如果要表示整数或小数,分为以下三种情况:
- 纯整数:例如整数100,小数点其实在最后一位,所以忽略不写
- 纯小数:例如:0.123,小数点固定在最高位
- 整数+小数:例如1.24、10.34,小数点在指定某个位置
定点数表示纯整数
对于纯整数100,由于小数点固定在最低位,假定我们以 1 个字节(8 bit)表示,用定点数表示如下(D为十进制缩写,B为二进制缩写):
100(D) = 01100100(B)
定点数表示纯小数
对于纯小数 0.125,由于小数点固定在最高位,同样以 1 个字节(8 bit)表示,用定点数表示如下:
0.125(D) = 0.00100000(B)
定点数表示整数+小数
这种情况下,我们需要先约定小数点的位置。依旧以 1 个字节(8 bit)为例,我们可以约定前 5 位表示整数部分,后 3 位表示小数部分。
- 对于数字 1.5 用定点数表示就是这样:
1.5(D) = 00001 100(B)
- 数字 25.125 用定点数表示就是这样:
25.125(D) = 11001 001(B)
这就是用定点数表示一个小数的方式,总结过程如下:
- 在有限的 bit 宽度下,先约定小数点的位置
- 整数部分和小数部分,分别转换为二进制表示
- 两部分二进制组合起来,即是结果
但是定点数存在缺陷,对于一个固定位数,不管如何约定小数点的位置,都会存在以下问题:
- 数值的表示范围有限(小数点越靠左,整个数值范围越小)
- 数值的精度范围有限(小数点越靠右,数值精度越低)
总的来说,就是用定点数表示的小数,不仅数值的范围表示有限,而且其精度也很低。要想解决这 2 个问题,所以人们就提出了使用「浮点数」的方式表示数字。虽然定点数表示数字,存在以上说的这些问题,但也只是在表示小数的场景下。如果只是用于表示整数,还是非常方便的。所以,现代计算机中一般使用定点数来表示整数。
浮点数
对于定点数,其中「定点」指的是约定小数点位置固定不变。那浮点数的「浮点」就是指,其小数点的位置是可以是漂浮不定的。
浮点数表示格式:
V = (-1)^S * M * R^E
其中各个变量的含义如下:
- S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负
- M:尾数,用小数表示,例如 8.345 * 10^0,8.345 就是尾数
- R:基数,表示十进制数 R 就是 10,表示二进制数 R 就是 2
- E:指数,用整数表示,例如前面看到的 10^-1,-1 即是指数
浮点数标准
1985年,IEEE 组织推出了浮点数标准,就是我们经常听到的 IEEE754 浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:
- 单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit
- 双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit
为了使其表示的数字范围、精度最大化,浮点数标准还对指数和尾数进行了规定:
-
尾数 M 的第一位总是 1(因为 1 <= M < 2),因此这个 1 可以省略不写,它是个隐藏位,这样单精度 23 位尾数可以表示了 24 位有效数字,双精度 52 位尾数可以表示 53 位有效数字
-
指数 E 是个无符号整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255。但因为指数可以是负的,所以规定在存入 E 时在它原本的值加上一个中间数 127,这样 E 的取值范围为 -127 ~ 128。表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024。
除了规定尾数和指数位,还做了以下规定:
- 指数 E 非全 0 且非全 1:规格化数字,按上面的规则正常计算
- 指数 E 全 0,尾数非 0:非规格化数,尾数隐藏位不再是 1,而是 0(M = 0.xxxxx),这样可以表示 0 和很小的数
- 指数 E 全 1,尾数全 0:正无穷大/负无穷大(正负取决于 S 符号位)
- 指数 E 全 1,尾数非 0:NaN(Not a Number)
以25.125 为例,转换为标准的 float 浮点数:
- 整数部分:25(D) = 11001(B)
- 小数部分:0.125(D) = 0.001(B)
- 用二进制科学计数法表示:25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B)
得: S = 0,尾数 M = 1.001001 = 001001(去掉1,隐藏位),指数 E = 4 + 127(中间数) = 135(D) = 10000111(B)。填充到 32 bit 中,如下:
浮点数精度损失
如果我们现在想用浮点数表示 0.2,它的结果会是多少呢?
0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...
所以 0.2(D) = 0.0011 0011…(B)。 因为十进制的 0.2 无法精确转换成二进制小数,而计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
浮点数的范围和精度
- 单精度浮点数 float (float32):
-
它能表示的最大二进制数为 +1.1.11111…1 * 2^127(小数点后23个1),而二进制 1.11111…1 ≈ 2,所以 float 能表示的最大数为 2^128 ≈ 3.4 * 10^38,即 float 的表示范围为:-3.4 * 10^38 ~ 3.4 * 10 ^38。
-
精度:float 能表示的最小二进制数为 0.0000…1(小数点后22个0,1个1),用十进制数表示就是 1/2^23。
- 双精度浮点数double(float64):
-
double 能表示的最大二进制数为 +1.111…111(小数点后52个1) * 2^1023 ≈ 2^1024 = 1.79 * 10^308,所以 double 能表示范围为:-1.79 * 10^308 ~ +1.79 * 10^308。
-
double 的最小精度为:0.0000…1(51个0,1个1),用十进制表示就是 1/2^52。
| 占字节数 | 数值范围 | 十进制精度位数 | |
|---|---|---|---|
| float | 4 | -3.4e-38~3.4e38 | 6~7 |
| double | 8 | -1.79e-308~1.79e308 | 14~15 |
如果内存不是很紧张或者精度要求不是很低,一般选用double。14位的精度*(是有效数字位,不是小数点后的位数)通常够用了。注意,问题来了,数据精度位数达到了14位,但有些浮点运算的结果精度并达不到这么高,可能准确的结果只有10~12位左右,往后的几位呢?自然就是不可预料的数字了,即使是理论上相同的值,由于是经过不同的运算过程得到的,他们在低几位有可能(一般来说都是)*是不同的。这种现象看似没太大的影响,却会对==(判断相等)产生致命的影响。注意,C/C++中浮点数的==需要完全一样才能返回true。