摆理论和套公式其实也是为了说明问题,解释原理,不过有时却适得其反。
前一个主题介绍了整数在计算机里的存储形式,浮点数,即小数,例如 3.14、2.71828 等,看起来和整数相近,而二者在计算机中存储的形式大为不同,作为一种常用的数据类型,来了解了解其中的本质,这个主题我们一起来摸摸「浮点数」的底。
浮点数有多种类型,但是我们写代码时常用到的有两种,按照所占长度分为:一个是单精度,占用 32 个二进制位,另一个是双精度,占用 64 个二进制位。不管是什么类型、占用多少位,存储的模式都是类似的,而这也正是我们想要摸的「底」。
1. 化身为二进制的浮点数
首先明确一点,不管什么类型的数据,整数也好,浮点数也罢,在计算机中都是以二进制形式存储,即 01001、10110 这样的形式。
那么,二进制的浮点数该如何表示呢?
在此之前,我们先来看下十进制的小数 22.71是如何表示的:小数点左边第一位为个位,表示 1,第二位为十位,表示 10;小数点右边第一位为十分位,表示 1/10,即 10^-1,第二位为百分位,表示 1/100,即 10^-2,综合如下。
1 | 22.71 = 2*10^1 + 2*10^0 + 7*10^-1 + 1*10^-2 |
上面就是十进制浮点数的表示形式,而二进制与十进制整体相同,唯一区别就在于将 10 换成了 2,举个例子,如二进制浮点数 101.11:小数点左边第一位表示 2^0,即 1,第二位表示 2^1,即 2,第三位表示2^2,即 4;小数点右边第一位表示 2^-1,即 0.5,第二位表示 2^-2,即 0.25,综合如下。
1 | 101.11 = 1*2^2 + 0*2^1 + 1*2^0 + 1*2^-1 + 1*2-2 |
2. 自由转化的浮点数
既然,浮点数既可以用十进制表示,也可以用二进制表示,那么同一个浮点数如何在这两种进制之间自由转化呢?由二进制转为十进制就不用说了,上面刚刚用 101.11 演示完,而由十进制转为二进制很多人都是直接套公式却没想过原理,接下来说说推导公式的过程。这个转化过程需要将浮点数的整数部分和小数分别转化,之后再进行合并。
我们用 6.625 来举例说明。先看整数部分的 6,这个我们一眼就能看出来,结果是 110,但是如果换成 60、600,这些不能一眼看出的数呢?其实很多事情都是如此,将思考过程从「一眼就能看出的」的情况中抽象出来,就是计算方法。这个思考过程无非就是,想知道需要几个二进制位、每个二进制的值是多少,就可以表示 6。
为了便于理解,我们先看一下计算表示 758 需要几个十进制位以及每位的值是多少。
1 | 先从个位,即右起第一位开始: |
由此可得,求第 n 位的值时,用计算完 n-1 位的商对 10 取余数即可,当商为 0 时,结束,注意 n > 1。如上,就是十进制抽取方法的过程,也同样适用于二进制,区别就在于将 10 换成了 2,实际计算一下。
1 | 计算将 6 用二进制的值,同样从右起的第一位开始: |
与整数部分的计算类似,再看下小数部分的计算。同样,先用比较直观的十进制浮点数举例,从而从中总结规律。
1 | 计算 0.358 用几位十进制的浮点数表示,及每位的值的过程,从左开始, |
由此可得,计算第 n 位时,用计算完第 n-1 位的余数乘 10 再取商即可,当余数等于 0 时结束,注意 n > 1。紧接着,用我们从上面十进制例子中总结的方法计算下二进制。
1 | 计算 6.625 的小数部分 0.625 的二进制形式,从左开始, |
至此,我们完成了所有计算过程,整数部分:110,小数部分:101,最终结果为:110.101。
3. 如何存储
前面做了那么多铺垫,现在终于可以开始说存储了。存储之前,需要先把二进制的浮点数用科学计数法表示,也称规范化,即
N = ±a x 2^n , a∈ [1,2)
a 叫作尾数,n 叫作阶码。计算机存储的浮点数都是这种形式的,例如上面的 110.101 就需要变成 1.10101x2^2,而 0.1101 就需要变成 1.101x2^-1,这个变换的过程很简单。
对于浮点数 ±a*2^n,计算机将其分成了 3 个部分来进行存储,即存储正负的符号位、存储 n 的指数域和存储 a 的尾数域,是的,存储 a 的部分叫做尾数域。
对于长度为 32 位二进制的浮点数,从左起,第 1 位用来表示符号,0 代表正数,1 代表负数。接下来的 8 位,即第 2 位到第 9 位,用来表示指数。而从第 10 位开始一直到最后的 32 位,都用来表示尾数,对于科学计数法下的 a,其整数部分的值永远都是 1,所以这个 1 就省略了,因此 23 位尾数二进制只存储了 a 的小数部分,即 1.10101 的 10101,1.101 的 101。
和 32 位类似,长度为 64 位二进制的浮点数,从左起,第一位存储符号,接下来的 11 位存储指数,剩下的 52 位存储尾数,而尾数同样省略了 a 的整数部分,只存储小数部分。
4. 阶码
符号位,0 或 1,尾数域,采用原码表示,这两种比较简单明了,这里要单独说一说阶码。
在此之前,先回忆一下移码。所谓移码,实际上就是把负数映射到正轴上,通俗的说就是消灭负数。例如,4 个bit位能表示的范围,[-2^3, 2^3 - 1],即[-8, 7]。每个数字加上偏移量 2^3,即为移码。
阶码在计算机中也是以移码的形式存储的,区别在于这个偏移量。根据 IEEE 754 标准,k 位二进制位的解码,偏移量 bias 为 2^k-1 - 1,例如 32 位单精度浮点数,阶码为 8 位,则偏移量为 2^8-1 - 1 = 127。
N = (-1)^S x 2^E-Bias x (1+M)
其中,S为符号位的值,E为指数的值,M为尾数域的值,Bias为移码偏移量。
举个例子,-6.75,单精度浮点数,转化为规范化二进制为:-110.11 = -1.1011 x 2^2
符号位 S = 1,
阶码 E = 2 + Bias = 2 + 127 = 129
尾数 M = 1.1011 - 1 = 1011 (省去小数点)
1 10000001 10110000000000000000000 (尾数域:4 位精度 + 19 个 0,共 23 位)
至此,浮点数就介绍完了。