JAVASE基础教程
——
Java语法基础卷1Java基础语法
补充1IEEE754标准

已最新版本|V4.0
长风
COPYRIGHT ⓒ 2021. 王道版权所有

前言

>(red!)

首先,浮点数和IEEE754标准不是学习Java的必备知识,本节知识完全不了解,也不影响做一名(基本的)Java开发。

学习本节需要有一些计算机常识知识,还涉及到《计算机组成原理》等计算机专业知识。建议没有计算机基础或者学习闲余时间不多的同学,不要浪费时间学习本章节,应该把学习时间花在刀刃上。

以上是劝退,但还是要说一下学习它的必要性:

浮点型,终究是Java当中常见的数据类型,理解它的原理对我们做开发是有一定帮助的,属于纯粹的内功。即便现在不学,以后还是需要回来补的。

最后还是要说,本节内容不影响成为Java开发,也几乎不会作为面试问题,请结合自身情况学习。

十进制和二进制的移位

>(green!)

提到计算机当中数的表示,二进制是跑不掉的,现在做以下约定:

  1. 一个数的下标表示它的进制,无下标的数默认就是十进制,比如1、123等等都是十进制的数。

    有下标的数,如102 表示二进制的10,即十进制的数字2。

  2. 一个数的上标和正常的数学公式一样,表示它的幂运算,比如102表示10的2次方,即100。

  3. 一般不会出现一个数既有上标也有下标,如有会括号说明它的含义。

除了以上设定外,本文中还会涉及进制转换的问题,如果不会请自行学习一下,或者直接使用Windows程序员计算器转换进制。

在Windows操作系统下,按:”Win + R“,输入”calc“,然后运行,即打开计算器功能:

Windows计算器

(Mac也有类似的功能,不知道的百度即可调出来!)

>(red!)

下面看一个推导过程:

12.34 = 1 × 101 + 2 × 100 + 3 × 10-1 + 4 × 10-2 = 12.34

一个十进制数可以做以上分解,那么对于一个二进制数,可以做以下分解:

101.112 = 1 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 1 × 2-2 = 4 + 0 + 1 + 1/2 + 1/4 = 5.75

上式反过来:

5.75 = 5 + 3/4 = 4 + 1 + 1/2 + 1/4 = 1 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 1 × 2-2 = 101.112

通过上述式子,不难看出:

十进制每高一位(右移一位),就表示将该数乘以10,每低一位(左移一位)就表示该数除以10

二进制每高一位(右移一位),就表示将该数乘以2,每低一位(左移一位)就表示该数除以2

这两个结论虽然不那么起眼,但是对我们而言却是十分重要的,下面我们要用到。

十进制的科学计数法

>(green!)

科学计数法是一种数字的表示方法,普通的数字用科学计数法表示并无多大优势,但是一个极大或者极小的数使用科学计数法来表示就会很有优势。

例如:

0.0000000000000000000000112332 = 1.12332 × 10-23

112332000000000000000000 = 1.12332 × 1023

等号右边就是科学计数法的表示形式,即 a * 10n,在计算机当中经常写为aen或者aEn,其中:

  1. |a| >= 1 且 |a| < 10
  2. n一定是整数
  3. e或E表示乘以10的多少(n)次方

在计算机领域,在计算中存储极大或者极小数时,使用科学计数法有以下优势:

  1. 长度显著变短(这很重要,意味着在计算机中可以节省内存空间)
  2. 可以直观的比较大小(数量级、精度等等一目了然)

当然十进制的科学计数法,大家早已经十分熟悉,我们今天就推导一下二进制的科学计数法。

二进制的科学计数法

>(green!)

根据十进制的科学计数法推导,如果用科学计数法表示二进制,格式为:

a2 × 2n

其中:

  1. 基数从原先的10变为2
  2. 底数a,变成了一个二进制数
  3. |a| >= 1 且 |a| < 2,也就说a应该是(1.xxx)2的二进制数
  4. n是一个整数

例如:

5.75 = 101.112 = 1.01112 × 22 (右移几位,乘几个2)

0.1875 = 0.00112 = 1.12 × 2-3 (左移几位,除以几个2)

明白了二进制的科学计数法表示方式后,浮点数的IEEE754标准表示方式就基本可以理解了。

IEEE754表示的浮点数

>(green!)

IEEE754标准下表示的浮点数,可以简单的理解成二进制的科学计数法表示形式。IEEE754标准,规定了以下主要两种浮点数的表示形式:

  1. 单精度浮点数(内存空间占用32位)
  2. 双精度浮点数(内存空间占用64位)

下图就是IEEE754表示的浮点数的具体形式(精度不同体现在位数的差别)我们结合二进制的科学计数法表示范式,来说明一下浮点数的三个部分。

IEEE754浮点数表示形式

对于二进制的科学计数法表示:a2 × 2n

IEE754标准浮点数可表示为:

  1. 符号位,计算机有符号数中,0表示正数,1表示负数。

  2. 阶码,规定为实际指数值(也就是上述式子中的n)加上一个偏移值常量,偏移值为2m-1 - 1,其中m为该阶码的位长。

    • 阶码就是n + 2m-1 - 1 这个数的二进制表示。
    • 单精度浮点数的阶码位长是8,即m = 8,偏移值为127
    • 双精度浮点数的阶码位长是11,即m = 11,偏移值为1023
    • 阶码,是通过某个二进制数的科学计数法的,指数位n计算出来的,所以阶码也常常被称为指数位(我也喜欢这么叫)
  3. 尾数,用于存储科学计数法中的有效数字(也就是上述式子中的a2),它是用原码表示的!

    • 尾数常常被称之为小数位,因为它就是a2的二进制表示,如果长度不够后面补0。
    • 很容易发现 a2 总是1.xxx,总是1.开头的。所以IEEE754标准当中,尾数总是默认隐含1.,计算的时候不要忘记这个隐含1.

>(red!)

单精度和双精度浮点数,具有不同的表示方式,主要是位数的差异带来的不同:

单精度浮点数,长度为32位:

  • 最高的一位是符号位
  • 符号位后面8位是阶码(指数位),偏移值常量是127
  • 最后的23位是尾数(小数位)

双精度浮点数,长度为64位:

  • 最高一位是符号位
  • 符号位后面11位是阶码(指数位),偏移值常量是1023
  • 最后的52位是尾数(小数位)

定点数

>(green!)

实际上当你了解了上述浮点数的表示形式后,那么就可以解释一个非常重要的问题了:浮点数为什么叫浮点数?既然有浮动的点数,那么有没有不动的点数?

概念

>(green!)

对于用a2 × 2n形式表示的二进制数:

a2决定了这个数的有效数字是什么,而它的大小(也就是小数点的位置)是由指数位n来决定的。

  1. 如果这个n是一个固定的数,也就意味着该数的小数点位置是不变的

    • 这种表示方式的数字就被称之为"定点数"
    • 定点数的表示中,都会直接约定小数点的位置。
  2. 如果这个n不是固定的数,也就意味着该数的小数点位置是可变的

    • 这种表示方式的数字就被称之为"浮点数"
    • IEEE754标准下表示出来的数字的小数点位置显然是可变的,它是一种浮点数的表示方式。

区别

浮点数和定点数是计算机中数字的两种不同的存储表示方式:

  1. 定点数(Fixed-point Number)
  2. 浮点数(Floating-point Number)

下面来看一下定点数的表示数值的方式。

定点数如何表示数字?

>(green!)

定点数可以存储小数和整数,由于小数点位置固定不变,所以存储时小数点不进行存储,只需要约定位置即可。

定点数约定小数点位置,可以分为以下三种情况:

  1. 纯整数:例如整数100,小数点约定在最低位即可。
  2. 纯小数:例如:0.123,小数点约定在最高位即可。
  3. 整数 + 小数:例如1.23、12.3,需要指定小数点在某个位置

现在对定点数的表示做以下定义:

假设以机器字长 n 位表示定点数,从右至左,从高位到低位分别为 Xn、Xn-1...X3、X2、X1 其中 Xn是符号位,取值 0 和 1 分别表示正号和负号。

如此,对于任意一个定点数 X = XnXn-1…X2X1,在定点机器中可表示为:

定点数的表示

  1. 如果 X 表示的是纯小数,那么小数点位于 Xn 与 Xn-1 之间。
  2. 如果 X 表示的是纯整数,那么小数点位于 X1 的右边。
  3. 如果X表示的是整数 + 小数,那么小数点可以约定在有效数字的任意一位。
  4. 最后还需要注意的是,计算机中存储数值都是以补码的形式存储的。

定点数表示纯整数和小数

>(green!)

纯整数:

对于纯整数100,由于小数点固定在最低位,假定我们以 1 个字节(8 bit)表示,用定点数表示如下:

100 = 0 11001002

-53 = 1 10010112(补码)

其中,最高位是符号位,小数点约定在最后一位,实际上计算机中的整数就是以定点数形式存储表示的

 

纯小数:

对于纯小数 0.125,由于小数点固定在最高位,同样以 1 个字节(8 bit)表示,用定点数表示如下:

0.125 = 0 01000002

 

整数 + 小数:

这种情况下,我们需要先约定小数点的位置

以 1 个字节(8 bit)为例,第一位为符号位,约定之后 5 位表示整数部分,最后 2 位表示小数部分

对于数字 1.5 用定点数表示就是这样:

1.5 = 0 00001 102

对于数字 26.5 用定点数表示就是这样:

26.5 = 0 11010 102

总结

>(green!)

以上就是用定点数表示一个小数的过程,其过程可以做以下总结:

  1. 在有限的 bit 位数限制下,先约定小数点的位置
  2. 整数部分和小数部分,分别转换为二进制表示
  3. 两部分二进制组合起来,即是结果

显然使用定点数去表示一个数是非常简单的,但是却并不是没有代价的,例如以下问题:

>(red!)

如上文所说,我们约定第一位为符号位,之后 5 位表示整数部分,最后 2 位表示小数部分后,此时

  1. 整数部分的二进制最大值只能是 11111,即十进制的31
  2. 小数部分的二进制最大只能表示 0.11,即十进制的 0.75

这样表示的数值很有限,而且因为位数限制,能够表示的小数是离散的,不是连续的!

于是可以做一下总结:

对于有限位数的定点数来说:

  1. 小数位越短,表示的数精度就越低,无法表示更多小数点后面的位数,比如2位小数显然表示不了X.00001

    • 同时整数位就越大,表示的数值就越大。
  2. 小数位越长,表示的数精度就越大,就能够表示更多小数点后面的位数

    • 同时整数位就越短,能够表示的数值就越小。

如果想表示数的范围更大或者精度越高,可以考虑:

  1. 扩大 bit 位数:例如使用 2 个字节、4 个字节,这样整数部分和小数部分宽度增加,表示范围和精度都提升了
  2. 改变小数点的位置:小数点向后移动,整个数字范围就会扩大,但是小数部分的精度就会越来越低

由此我们发现,不管如何约定小数点的位置,都会存在以下问题:

  • 数值的表示范围有限(小数点越靠左,整个数值范围越小)
  • 数值的精度范围有限(小数点越靠右,数值精度越低)

总的来说,如果使用定点数表示小数,不仅数值的范围表示有限,而且其精度也很低。

所以在计算机中,普遍使用定点数表示纯整数,不使用它表示小数,而使用浮点数表示小数。

浮点数具体案例说明

>(green!)

以下案例都以32位单精度浮点数为准(64位和32位只有位数的差距,可以套用)

怎么知道我们手动转换出来的浮点数是否正确呢?为了验证这一点这里提供两种方案:

最简单的,找一个在线转换浮点数和十进制的网站

比如: http://www.styb.cn/cms/ieee_754.php

  • 也可以找一个IEEE754标准转换器(百度云自取)

链接:https://pan.baidu.com/s/1nj--X0OY8MopJtExK4v1Ow 提取码:b0tf

正数案例

>(red!)

正数是最常见的情况,比较容易理解。

将十进制整数转换成浮点数

>(green!)

78 = 010011102 = 1.001112 × 26

分析:

这个数是正数,显然符号位应该是0

二进制科学计数法的指数是6,那么阶码应该是 6 + 127 = 133 = 1000 01012(8位)

尾数是最简单的,就是00111,但是单精度浮点数的尾数应该是23位,后面的位要用0补满

最终得到的32位单精度浮点数表示为(分段表示):

0 -- 1000 0101 -- 00111000000000000000000

将十进制小数转换成浮点数

>(green!)

360.75 = 101101000.112 = 1.01101000112 × 28

分析:

这个数是正数,显然符号位就是0

二进制科学计数法的指数是8,那么阶码应该是 8 + 127 = 135 = 100001112(8位)

尾数就是 0110100011,不满32位,用0补满

最终得到的32位单精度浮点数表示为(分段表示):

0 -- 1000 0111 -- 01101000110000000000000

将浮点数恢复成十进制数

>(green!)

将下列单精度浮点数表示,恢复成十进制数

0 -- 01111101 -- 10000000000000000000000

分析:

符号位是0,显然是一个正数

阶码是01111101 = 125 ,显然实际的指数是125 - 127 = -2

尾数实际上就是1,加上隐含的1,那么有效数字a就是1.12

因此,该浮点数的二进制科学计数法表示形式为:1.12 × 2-2 = 0.0112 = 0.375

负数案例

>(green!)

负数可能具有一定的理解难度,所以仅给出一个案例。

将十进制负整数转换成浮点数

>(green!)

-16 = -100002 = -1.0 × 24(原码表示)

分析:

是一个负数,显然符号位是1

阶码是 4 + 127 = 131 = 100000112

尾数是0,全部都是0

最终得到的32位单精度浮点数表示为(分段表示):

1 -- 1000 0011 -- 00000000000000000000000

总结浮点数表示步骤

>(green!)

虽然我们不知道为什么这么做,但是现在我们已经可以对十进制转换成IEEE754浮点数的步骤做一个总结:

  1. 将十进制数值转换为二进制数值。
  2. 将二进制数值转换为它的科学记数法表示形式,注意直接使用原码形式(正负数都一样)
  3. 确定符号位:如果是正数,符号位为0,如果是负数,符号位为1
  4. 计算阶码:将科学计数法的指数值加上偏移值(单精度为127,双进度为1023),再把这个数转换成8位二进制
  5. 计算尾数:忽略有效数字的整数部分1(有效数字整数部分总是1,约定忽略掉),将有效数字的小数部分作为尾数,如果不足位数(单精度23位,双精度52位)则用0在右边补满
  6. 这样,前面5步,我们就得到了一个符合IEEE754标准的二进制浮点数表示。

浮点数的精度和表示范围

>(green!)

学到这里,实际上你还有很多问题没有解决,还有很多困惑不明白,但是我们先放下困惑,用现有的知识来解决两个最重要的疑惑:

  1. 浮点数的表示范围有多大?
  2. 浮点数的精度有限制吗?

为了搞清楚这两个问题,我们需要首先研究一下浮点数的精度问题。

精度

>(green!)

显然,浮点数肯定是有精度的,因为浮点数能够表示的有效数字(尾数)是有限的,超过的部分就无法表示

float的精度

>(green!)

尾数都是用原码直接表示的,隐含整数位1,不带符号,其中float的尾数是23位

于是:

float的尾数最大值是:1111111111111111111111112 (23个1),也即是0~223

即范围是 0~8388608(十进制数)

这个数是一个比较大的7位数,所以float的有效小数位是6~7位的,其中6位是绝对正确的,7位多数情况正确

而如果直接看有效数字(因为隐含1),则应该是7~8位,其中7位是绝对正确的,8位多数正确

注意以上说的精度都是针对十进制数来的,下面的double也是一样。

double的精度

>(green!)

比个葫芦画个葫芦,double和float差不多的,只不过double的尾数是52位的。

于是:

double的取值范围是0~252 ,即0~4503599627370500

这个数是一个16位数,则有效小数位是15~16,其中15绝对准确,16一般都准确。

如果加上隐含的1,则有效位数是16~17,其中16绝对准确,17一般准确。

表示范围

>(green!)

尾数位用于决定浮点数的精度,那么指数位(阶码)就用来决定浮点数的表示范围。要想学习这个东西,咱们要先看几个问题和相关概念。

移码

>(green!)

为什么阶码的计算要加上一个偏移量?

阶码在存储的时候要加上偏移量,恢复的时候还要再减回来,来回折腾有必要吗?答案是肯定的,凡事都有两面性,有利有弊,这么设计的好处是什么呢?

阶码,实际上是二进制科学计数法表示数字的指数,这个指数在正常情况下可正可负。这样的话,当我们比较两个浮点数的大小时,不仅要考虑数值位(尾数)本身的大小和符号,还需要考虑指数的符号然后才能比较大小,逻辑上显然是比较麻烦的。

说到这里,可能有些同学会觉得并没有什么麻烦,-123和124我一眼就能看出谁大谁小,但是不要忘了我们说的比较是计算机比较大小,而不是人类。所以为了更好的比较阶码大小,最理想的情况下,我不希望有负号。

在深入探究这个问题之前,我们可以再探讨一个问题:计算机中存储负数为什么要使用补码而不是原码?

存当然是可以存的,但是如果直接存储负数原码会给计算带来困难。计算机为了设计简单,对于数值的加减运算都是带符号位的,也即减去一个正数相当于加上一个负数如果使用原码直接进行运算,得到的结果可能是不正确的,例如:

1 - 2 = 0000 00012 + 1000 00102 = 1000 00112(原) = -3

以上运算结果显然是荒谬的,但是如果使用补码存储负数,就不会出现这个问题,例如:

1 - 2 = 0000 00012 + 1111 11102 = 1111 11112(补) = 1000 00012(原) = -1

综上,使用补码存储负数会让人更难以去计算思考,但是:

  1. 对计算机而言,使用补码,能够仅使用加法指令就实现数值的加减运算
  2. 就可以简化CPU的指令集,简化CPU设计,提升运算的效率

以上两点,充分说明了我们学习计算机知识要站在计算机的角度,而不是人,这在很多场景中都是适用的。

再回到,我们阶码的问题上。 想一下:如果指数并不是直接存储而是加上一个偏移值,保证它一定是正数是不是就更有利于计算机比较它们的大小了呢?答案是肯定的,指数加上一个偏移值之后,无论指数部分是正是负,都可以转换成非负数。 这样的,将真值映射到正数域的数值(真值在数轴上正向平移一个偏移量),称为移码。

使用移码来比较两个真值的大小,只要高位对齐后逐位比较即可,不用考虑符号位问题。

 

偏移值

>(green!)

在明白为什么要有偏移值后,我们还有一个问题:为什么偏移值是2m-1 - 1而不是2m-1又或者别的值呢 ?

8位二进制有符号数的取值范围是[-128,127],也是32位浮点数指数可能的取值范围

8位二进制无符号数的取值范围是[0,255],也是32位浮点数阶码可能的取值范围

要使指数有符号数[-128,127],变为阶码的无符号非负数[0,255]需要加上偏移量128,为什么实际却加127呢?

>(red!)

为了解释这一问题,引用一段教材的原文:

"当阶码E为全0且尾数M也为全0时,表示的真值X为零,结合符号位S为0或1,有正零和负零之分。当阶码E全1且尾数M为全0时,表示的真值X为无穷大,结合符号位S为0或1,也有+∞和-∞之分。

这样在32位浮点数表示中,要除去E用全0和全1(255)表示零和无穷大的特殊情况,指数的偏移值不选128(10000000),而选127(01111111)。对于规格化浮点数,E的范围变为1到254,真正的指数值e则为-126到+127。因此32位浮点数表示的绝对值的范围是10-38~1038(以10的幂表示)。" ——引自白中英《计算机组成原理》

>(red!)

当然以上一段话,你可以完全不理解。这里要先引入一个不是很新的概念:规格化浮点数。

我们上面提到的浮点数表示方式,实际上指的都是规格化浮点数。而浮点数一共有5种表示形式,如下表所示:

对于以下浮点数表示形式:

IEEE754浮点数表示形式

注:NaN(Not a Number,非数)是计算机科学中数值数据类型的一类值,表示未定义或不可表示的值。常在浮点数运算中使用,首次引入NaN的是1985年的IEEE 754浮点数标准。

IEEE754浮点数的五种表示方式
浮点数形式阶码尾数描述(m是浮点数的位长)
00阶码是0,尾数表示的小数部分也是0,那么这个数是 ±0(正负取决于符号位)
非规格化0非0阶码是0,尾数表示的小数部分非0时,此时有效数字的整数部分为固定值0,这就是浮点数的非规格化表示形式,它用于表示一个非常小非常接近0的数(指数偏移值为2m - 2)
规格化[1,2m - 2]任意阶码不包括全0和全1,尾数表示的小数部分为任意数值,此时有效数字的整数部分为固定值1,这就是浮点数的规格化表示形式,它是浮点数的主要表示形式,日常使用的绝大多数数值都可以用它来表示(指数偏移值为2m - 1)
无穷2m - 10阶码是2m - 1(全1),尾数表示的小数部分为0,那么这个数是 ±∞(正负取决于符号位)
NaN2m - 1非0阶码是2m - 1(全1),尾数表示的小数部分为非0,那么这个数是NaN(非数)

当你了解上述表格知识后,我们就可以来掰扯掰扯为什么32位规格化浮点数阶码的偏移值是127而不是别的了。

由于没有找到明确的官方解释,这里提供几个猜想的原因:

  1. 由于阶码的范围从[0,255]变为了[1,254],如果偏移值为128的话,那么实际指数范围就是[-127,126],这样不是不可以。但是而如果偏移值改为127的话,该浮点数就能表示更大的范围。
  2. 阶码是127所表示的浮点数取值范围更加对称,比128更合适。
  3. 以上两点可以查验表格数据去验证。

>(red!)

当然,以上解释可能都不太完美,也许这只是设计者的一个随手之举。总之无论如何请记住,以下规定:

  1. 规格化浮点数阶码的范围是 [1,2m - 2],对于单精度浮点数是[1,254]
  2. 偏移值是2m - 1,对于单精度浮点数是127
  3. 单精度浮点数float的实际指数范围是[-126,127]

非规格化浮点数

>(green!)

既然提到了规格化浮点数,那这里我们也简单了解一下非规格化浮点数:

一般是某个数字相当接近零时,才需要使用非规格化形式来表示,这时它的阶码是0,尾数为非0

IEEE754标准规定:

非规格化浮点数的指数偏移值,比规格化浮点数的小1,也就是偏移值为2m - 2

拿单精度浮点数为例,偏移值为126,由于阶码固定为0,所以实际指数固定为-126

非规格化浮点数解决填补了绝对值意义下最小规格数与零的距离,避免了突然式下溢出(abrupt underflow)

下溢出:指当要表示的数据的绝对值小于计算机所能表示的最小绝对值时,出现无法表示数值的情况

关于突然式下溢出,我们在后面还会论述。

实际上,当你学习到这里,对于浮点数的表示范围已经有了认识,接下来我们就以单精度为例,抛砖引玉。

单精度浮点数的各种极值

>(green!)

表格说明:

  1. 表格上半部分为负数,下半部分为正数,以0为间隔
  2. 表格上方这个数值越小,表格下方这个数值越大(不是绝对值,NaN除外)
  3. 为了描述方便,最值指的都是绝对值大小而不是数值本身大小
  4. 该表格完全根据上面浮点数表示形式的表格推导而来
  5. 单精度浮点数的位数是8位
单精度浮点数的各种极值
浮点数表示形式符号位实际指数(阶码)有效数字 (含隐藏位)表示数值
负无穷1128(255)小数位是0-∞
规格化最大值1127(254)2 - 2-23-(2-2-23) × 2127 = -3.4E+38
规格化最小值1-126(1)1.0-2-126 = -1.18E-38 (-1.17549435E-38)
非规格化最大值1-126(0)1 - 2-23-(1-2-23) × 2-126 = -1.18E-38 (-1.17549421E-38)
非规格化最小值1-126(0)2-23-2-23 × 2-126 = -1.4E-45
负零1-127(0)0-0.0
正零0-127(0)0+0.0
非规格化最小值0-126(0)2-232-23 × 2-126 = 1.4E-45
非规格化最大值0-126(0)1 - 2-23(1-2-23) × 2-126 = 1.18E-38 (1.17549421E-38)
规格化最小值0-126(1)1.02-126= 1.18E-38 (1.17549435E-38)
规格化最大值0127(254)2 - 2-23(2-2-23) × 2127 = 3.4E+38
正无穷0128(255)小数位是0+∞
NaN0或1128(255)小数位非0非数

注:

  • 非规格化最大值和规格化最小值非常接近,但是它们并不是一个值
  • E表示十进制科学计数法,即乘以十的多少次方
  • 双精度浮点数和单精度浮点数表示方式是一致的,可以类推一下,这里不再给出

最后可能有些同学还会有疑惑:有效数字当中的2-23是怎么来的呢?

举个例子:

规格化最大值的有效数字是:1.111...12(小数点后23个1)

这个数可以转换成:22 - 0.000...12(小数点后22个0)

稍做转换:22 - 12 × 2-23 (转换成了科学计数法)

整体转换成十进制:2 - 1 × 2-23

规格化浮点数最大值的有效数字为:2 - 2-23

float的表示范围

>(green!)

当你看到这里,应当已经明白:浮点数的取值范围已经不能够简单的,去使用一个区间描述了,以float为例子:

  1. float的规格化形式包含两个区间:[-3.4E+38,-1.18E-38] 和 [1.18E-38,3.4E+38]

    • 规格化形式,无法表示相当接近0的两个区间[0,±1.18E-38)
  2. float的非规格化形式也包含两个区间:[-1.18E-38,-1.4E-45] 和 [1.4E-45,1.18E-38]

    • 非规格化形式用于表示相当接近0的数值,但是它仍然有不能表示的区间[0,±1.4E-45)
  3. 我们日常使用float几乎只会使用规格化浮点数,所以很多书籍里也会直接指出float的表示范围为:[-3.4E+38,3.4E+38]

    • 现在你已经知道上述说法是具有一定的瑕疵的,但是它在多数情况下是正确的。

当然,double和float的表示形式是一样的,无非位数不同,可以类推一下,这里不再赘述。

小结

>(green!)

以上,我们了解了IEEE754标准下浮点数的表示范围和精度两大问题,这里做一些总结和额外的说明

  1. 浮点数集合当中的所有元素都是有序的(NaN除外)

  2. 浮点数的零有正负之分,但两者是相等的。但是在实操上有些许差距,比如

    • 1.0 ÷ (+0.0) = +∞
    • 1.0 ÷ (-0.0) = -∞
  3. 我们常使用的规格化浮点数不能表示一个很小的,接近0的数,这会产生一些问题

    在使用规格化浮点数的情况下,正数当中的第一小,第二小,第三小的数字分别是:

    • 2-126
    • ( 1 + 2-23 ) × 2-126
    • ( 1 + 2 × 2-23 ) × 2-126

    这些数字显然不是连续的,而是间隔2-126 × 2-23 = 2-149,从第一个开始,以后每一个都是

    但是你会发现规格化浮点数的最小值和0的差距不是2-149,而是2-126,这个数要大223

    这样浮点数的区间就不是连续的了:

    规格化表示时,正数往0靠拢,逐个减小2-149,但是到了最小值后,突然减小了2-126变成了0

    以上情况,就是我们上面提到的突然式下溢出(abrupt underflow)

  4. 为了解决突然式下溢出的问题,IEEE采用了Intel公司力荐的渐进式下溢出(gradual underflow)

    实际上就是在规格化的最小值和0之间,加入了浮点数的非规格化表示

  5. 在使用非规格化浮点数的情况下,正数当中的第一小,第二小,第三小的数字分别是:

    • 2-23 × 2-126 = 2-149
    • (2 × 2-23) × 2-126
    • (3 × 2-23) × 2-126

    这样设计后:

    0到非规格化浮点数最小值的间隔就变成了2-149,并且非规格浮点数逐个之间的间隔也是2-149

    而非规格浮点数的最大值为(1-2-23) × 2-126,这个数和规格化浮点数的最小值2-126之间的间隔也是2-149

我们在这里反复提到了浮点数的间隔问题,它的实质就是:浮点数在数轴上是规格分布的吗?

如果你已经掌握了本节上述的所有内容,那么这个问题不难回答:

  • 非规格化浮点数只能表示接近0的特别小的数,在[-1.18E-38,1.18E-38 ]这个区间内数值是连续的

    • 间隔为2-149
    • 非规格浮点数的阶码是相同的,都是0
  • 规格化浮点数在阶码相同的一组数区间内是连续的

    • 但是不同阶码的浮点数之间显然是不连续,而且随着阶码越来越大,数值越来越远离原点(0)

    • 每个阶码区间内连续的间隔越来越小,精度越来越低

      • 阶码为1,数的间隔为2-149,精度也是
      • 阶码为2,数的间隔就变成了2-148,精度也降低了
    • 而不同阶码区间的最大值和最小值的间隔以阶码小的为准

      • 阶码为1的最大值和阶码为2的最小值,间隔2-149
  • 显然非规格化数是均匀分布的,精度为2-149,且自然过渡到规格化数,

  • 规格化数不是均匀分布的,规格化数的阶码每加一,精度就减半,但是阶码越大表示的数(绝对值)会更大

无穷大在两头,0与无穷大之间大部分是规格化数,只有非常接近0的少部分是非规格化数。

而且,越是靠近0的地方,数与数之间的间隔越小,到了非规格化数时,间隔是2-149

相反,越是往无穷大的地方,数与数之间的间隔越大。

当越过非规格式化最小值时,下溢到0。当越过规格式化最大值时,上溢到无穷大。

>(red!)

这篇文章就进入尾声了,我们再回头看一个问题:为什么偏移值是2m-1 - 1而不是2m-1又或者别的值呢 ?

无需长篇大论,思考一下为什么规格化浮点数和非规格化数之间能够平滑过渡?

这实际上和偏移值设置为127也是有关系的,127的偏移值保证了规格化最小值和非规格化最大值的平滑过渡。

后记

>(red!)

坦白来说,这篇文章写到这里已经非常长了,作为IEEE754的入门学习文章它甚至有点过长了。如果你能够学到这里,不得不说你非常有毅力,非常有钻研精神,很不错。

看完IEEE754标准的设计规范,我相信你也会感概它的设计是这么优美,这么巧妙。关于它的一些其它问题,就不在文章中继续啰嗦了,如果感兴趣可以自行学习查找。

这篇文章是花费业余时间书写的,甚至不是连续时间写完的,相信出现错误也完全是可能的,欢迎指正!

最后送大家一段我非常喜欢的话,它节选自物理选修3-1电磁场的前言,

曾经学习的时候既没有特别注意过,也没有真正去理解它,如今做了老师,再回头看不禁感概万千。

一首打油诗~

各位在编程的道路上也是一样,如果你还有些许不解和疑惑,不要着急,先继续往下学。终会有一天,一切都将豁然开朗。

The End

如有发现错误,欢迎联系老师更新!