注意:阅读本文前请务必了解基本的二进制知识,详情可见:《二进制运算从入门到入坟》

如果没有明确指出,本文均使用double作为例子

IEEE754

  IEEE 754是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。这个标准确定了以下内容:

  • 标准化正浮点数和负浮点数
  • 如何表示正负非规范化浮点数
  • 如何表示0
  • 如何表示无穷数(Infiniti)
  • 四种数值舍入规则

  同时IEEE 754还定义了四种浮点数格式:

  • 32位单精度(float)
  • 64位双精度(double)
  • (>= 43)位单扩展精度
  • (>= 79)位双扩展精度(一般用80位)

  因为后两种格式并不常用,所以这里只说前两种floatdouble

小数的二进制表示

  我们先来了解小数的二进制表示。现在我们将一个十进制小数:5.8125转换为二进制:

  第一步:先将整数部分转换为二进制101

  第二步:将小数部分转换为二进制,方法为将小数部分乘以2取整,直至变为1

1
2
3
4
5
6
0.8125 * 2 = 1.6250 ------ 取 1
0.6250 * 2 = 1.2500 ------ 取 1
0.2500 * 2 = 0.5000 ------ 取 0
0.5000 * 2 = 1.0000 ------ 取 1
------------------------------
最终结果为:1101

  第三步:得出答案101.1101

二进制科学计数法

  我们在小学时期就接触过“科学计数法”,即1.x * 10^b。这种方式就是十进制的科学计数法,为了了解浮点数的二进制表示方法,我们必须先知道二进制的科学计数法。

  比如我们要用科学计数法表示105,十进制的表示方式为1.05 * 10^2。想用二进制表示的话需要先将105转换为二进制1101001,这时我们可以知道其科学计数法的表示为1.101001 * 2^6。(注:这里没有把指数换成二进制)

  正如十进制的科学计数法中要求整数位在区间(0, 10)内一样,二进制的科学计数法要求整数位在(0, 2)内,也就是说只能为1

浮点数内存分配

总长度S-符号位(bit)E-偏移指数位(bit)M-尾数位(bit)
32bit(float)1823
64bit(double)11152

  存储形式见下图:

浮点数内存结构示意图

符号位

  符号位很简单,0表示正数,1表示负数。

指数位

  我们用e来表示指数,比如8.1357 * 109,这里e = 9

  指数位并没有直接存储指数e,而是存储了偏移后的指数E

关系式

  我们假定b表示偏移量,则有如下关系式:E = e + b

偏移量表

长度(bit)偏移量b(十进制[二进制])
32(float)127[1111111]
64(double)1023[1111111111]

  观察可以发现,偏移量的二进制表达式所有位均为1,位数为指数位长度 - 1,通过这个规律我们便可以求得偏移量的具体值。

为什么存储偏移量

  存储E而不直接存储指数原本的值e是为了简化运算。

  可以很明显的发现,一个浮点数需要两个符号位,一个用来标明整个数字的正负,另一个用来标明指数的正负。

  但是如果指数位上也存储一个符号位的话在进行浮点运算的时候就会变得非常复杂,所以为了简便起见,我们选择将所有指数加上一个特定的值,使所有指数都偏移成正数,这样就不用再区分正负了。

两个特殊值

  double的指数位有11位,可以表示[0, 2047]的数字,但是标准排除了最小值和最大值两个数字,所以指数表示的范围最终为[1, 2046],被排除的两个数字用来表示特殊的数据,后面会详细描述。

尾数位

  尾数位存储的内容是二进制科学计数法前方的数字,假如我们要存储5.8125

  我们首先要将5.8125转化为二进制,即101.1101,然后将小数点左移,变成1.011101 * 2^2

  所以,尾数位要存储的数字便是:1.011101

  我们可以注意到,尾数的整数部分一定为1,那么整数部分我们完全可以不进行存储,只存储小数部分即可(这里就是只存了011101),所以虽然double的尾数为只有52位,实际上却存储了53位的数据。

  如果尾数长度小于最大尾数长度,则在尾数后面补0

示例

  5.8125float二进制表示为:0_10000001_01110100000000000000000

  • 首位为符号位,表示这是一个正数
  • 后面跟着的八位10000001转换为十进制是129,减去127等于2,也就是e = 2
  • 最后面剩余位表示的是.011101,整数位补1就是1.011101,再把小数点向右移动e位就是101.1101
  • 101.1101转换为十进制就是5.8125

零的表示方法

  看了上面的内容,细心的读者可能会发现一个问题,这种表示方式如何表示0?因为尾数永远是1.*,所以常规表示方式永远也不可能表示出0这个数,最多尽量接近0。这时候上文提到的两个特殊值中的一个便派上了用场。当指数位和尾数位全部为0时表示该数为0,即0的二进制表达式为(float)0_00000000_00000000000000000000000(符号位可以为1)。

非规约数

  如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。

  IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127

  实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有规约浮点数的绝对值;即非规约浮点数比任何规约浮点数更接近0。因为规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0

无穷大的表示方法

  现在还有一个特殊值没有用到,即指数为2047的情况。规范规定,当指数位全部为1并且尾数位全部为0时表示该数是一个无穷数,根据符号位的不同分为正无穷和负无穷。

NaN

  NaN的全拼是“Not a Number”,顾名思义,其表示这不是一个有效的数字。指数位全为1但尾数位不全为0的数字均为NaN,可以看出NaN有许多表达式。在一个NaN与其它数字(包括NaN)判断是否相等时一定返回false,同时因为NaN内部是有值的,只是其不能正确地表示数字,所以其并不是一个空值,也不能使用空值来判断其是否为NaN

  为此,在math.h中定义了一个函数isnan(x),该函数用于判断数字是否为NaN。还有一个于其对应的函数isnormal(x),该函数用于判断数字是否为一个正常的数字,当数字为0、无穷数、NaN时会返回false,否则返回true


参考资料