8.4 如何表达实数

    至此,我们学习了用灯泡的点亮和熄灭来表达整数 12 的方法。接下来,我们来探讨如何表达 1.5、0.001 这样带有小数点的实数。

    12目前还没有接触到负数的表达方式,严格来讲这里说的是非负的整数。C 语言中,这种类型叫做无符号整数型(unsigned int)。

    定点数——小数点位置确定

    一种方法是确定小数点的的位置。比如,约定好把整数的小数点向左移动四位,最低四位就是小数部分。这样一来,1 变成 0.0001,100 变成 0.0100 即 0.01.

    这种方法有个问题,它无法表达比 0.0001 小的数,比如无法表达 0.00001。当然只要把约定改为把整数的小数点向左移动五位得到小数部分就可以,但这样针对每一个新的小数都要记一句新的约定很困难,而且还容易出错。

    那该怎么办呢?

    浮点数——数值本身包含小数部分何处开始的信息

    把人们很难记忆的问题交给计算机去做就解决了,让数值本身包含何处开始为小数部分的信息就好了。

    这是怎样一种思考方法

    假如用 16 盏灯泡来表达小数,16 盏中的 10 盏可以表达 0~1023 之间的数,这是三位有效数字的信息;其余的 6 盏灯可以表达 0~63 之间的数,用来表达小数点的位置。

    这种方法不仅可以表达小的数也可以表达大的数。如果把所有位数都用来表达整数,16 盏灯全部用上,最多只能表达 6 万多的数。把表达小数点位置的范围 0~63 减去 33,得到-33~30 这一范围。把-1 约定为小数点向左移动一位,即除以 10,+1 约定为小数点向右移动一位即乘以 10。这样一来,这种方法可以表达 1023 后面再跟 30 个零这么大的数 13。

    13要理解这是一个多么大的数,可以想象一下您浴缸里的水分子数量,它比这个还要大几个数量级。

    这就是现在一般使用的浮点数的基本思想。上一小节讲的确定小数点位置的方法被称为定点数,这个方法中的小数点是动的,所以被称为浮点数 14。

    14学过 C 语言的人都知道,实数是用浮点型(float)来处理的。float 这个名称就来源于浮点小数(floating point number)。

    以前关于浮点数有各种不同的约定,现在都标准化为 IEEE 75415。

    15官方名称为“IEEE Standard for Floating-Point Arithmetic (ANSI/IEEE Std 754-2008)"。IEEE 754 最早制定于 1985 年,后于 2008 年进行了修订。另外,在此标准规定有 5 种标准类型,这里仅仅说明了其中的单精度二进制浮点数。

    IEEE 754 中规定的浮点数的构成

    图 8.10 中的指数相当于小数点的位置,尾数相当于小数点以下有效数字的部分。

    左边那盏灯(最高比特位)16 代表了数的符号。该位为 0 时表示正数,为 1 时表示负数 17。

    16也可以称之为 MSB(most significant bit),最高有效位。
    17在标准中,零区分为正的零和负的零。

    接下来的 8 盏灯是表示位数的指数部分。指数部分作为整数理解的话可以表达 0~255 之间的数,减去 127 得到范围-127~128。-127 和 128 分别代表了零和无限大,剩下的-126~127 代表了小数点的位置。-126 是指小数点向左移动 126 位,127 是指小数点向右移动 127 位。

    其余的 23 盏灯是尾数部分,表示了小数点以下的部分 18。尾数部最左边的灯泡表示 1/2(二进制中的 0.1),接下来是 1/4(二进制中的 0.01)。请看图 8.10 中的 1.75 这个数,它等于 1+1/2+1/4,用二进制来表示就是 1.11。所以,1/2 位的灯泡和 1/4 位的灯泡都点亮。指数部分为 127(要减去 127 就是范围中的 0),这表示小数点的位置移动 0 位。这两点组合起来就是 1.75。

    18准确来讲,尾数是在二进制表达中为使得整数部分变成 1 而移动小数点得到的小数部分。

    接下来的数 3.5,用二进制来表示是 11.1。小数点向左移动一位就得到 1.11。所以它的尾数部分和 1.75 一样,1/2 位和 1/4 位点亮。指数部分变成 128(减去 127 就是范围中的 1)。3.5(二进制中的 11.1)其实就是 1.75(二进制中的 1.11)的小数点向右移动一位得到的数 19。而 7.0 则是由指数部分继续加 1 得到。

    19在二进制中,小数点移动一位进位不是 10 倍而是 2 倍。指数部分加 1,变成 2 倍,减 1 变成 1/2。

    空标题文档 - 图1

    图 8.10 IEEE 754 中单精度二进制浮点数的原理图

    问题点

    现今大家接触到的语言中,实数大多用浮点数 IEEE 754 表达。从实用角度来看,大部分情况下这没有任何问题。但是,这种方法要表达 3 除以 10 的答案时,十进制中可以确切表达出来的 0.3 在二进制中却变成了 0.0100110011001100110011……这样的无限循环小数,无论怎么写都有误差存在。正因为如此,会出现对 0.3 做十次加法并舍去某些位数后得到 2 这样的现象。

    JavaScript
    // 0.3做10次加法也得不到3
    > 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3
    2.9999999999999996

    // 舍弃后变成了2
    > Math.floor(0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3 + 0.3)
    2

    银行和外汇交易等涉及资金操作的场合尤其不欢迎这种系统行为,所以这些场合使用的是定点数或者加三码(excess-3)这样的十进制计 算方式 20。

    20一般也叫做二-十进制码,加三码(excess-3)就是其中一种。