IEEE 754规范: 五, 一些补充

本章讨论一些零碎的话题.

一. 其他导致浮点数存储不精确的原因

在第三章中我们提到过, 数学中的小数是连续的, 而计算机中的小数(准确来说是ieee754标准中的小数)是离散的.

这就会引发精度问题: 图中的绿色指针只能指向蓝点, 不能指向蓝点之间的数. 比如上面最右边的图, 绿色指针其实无法指向0.3, 当你想要指向0.3时, 实际上会被舍入为0.234, 即舍入到离它最近的蓝点对应的值.


而除此之外, 进制问题也会导致IEEE754浮点数存储不精确

简单来说就是: 有限长度编码下, 每种进制都有他们不能精确表示的值

比如: 10进制不能精确的表示1/3 (0.3333333.....)

十进制可以精确表示1/5 (0.2), 但二进制无法精确表示1/5

不能精确表示时,只能进行近似. 编码长度越长,近似程度越高

举例: 下图尝试用二进制表示0.2, 可以发现, 只能近似表示...

当你把一个十进制数存储到计算机中时, 实际上存储的是该数的二进制表示.

所以, 当你写入如下代码时:

float f = 0.2;

虽然0.2(十进制)远远没有到达32位浮点数的精度上限(7位精度), 但计算机其实无法精确地存储该数值, 因为0.2(十进制)无法使用二进制格式精确表示.

此时变量f对应的内存状态是这样的:

↑ 你键入的是0.2

↑ 内存中实际存储的是0.20000000298023223876953125

可以在c语言中验证一下:

可见十进制的0.2无法用二进制精确表示, 但十进制的0.5却可以用二进制精确表示.

二. 二进制的小数形式

有些同学可能会纳闷, 二进制为什么会有小数形式? 我常见的二进制都是整数形式啊, 比如十进制的9, 表示为二进制是1001, 怎么会有 1001.101 这种二进制的小数格式呢.

其实对于程序员来说, 这里确实比如容易让人困惑, 比如win10自带的计算机, 就不支持二进制小数:

二进制模式下, 小数点是不能用的

许多编程语言, 比如js, 也不支持直接使用二进制小数:

支持二进制整数, 但不支持二进制小数

但和十进制一样, 二进制其实也有小数形式, 而且很容易理解:

比如对于十进制数 78.23

十位: 7, 表示

个位: 8, 表示

十分位: 2, 表示2/10, 或说表示

百分位: 3, 表示3/100, 或说表示

这个十进制所表示的值是: 70 + 8 + 2/10 + 3/100


二进制数也是同理的:

比如对于二进制数 10.11

第一位: 1, 表示1 * 2^1 = 2

第二位: 0, 表示0 * 2^0 = 0

第三位: 1, 表示1 * 2^-1 = 0.5

第四位: 1, 表示1 * 2^-2 = 0.25

所以这个二进制表示的值, 其实就是十进制的2 + 0 + 0.5 + 0.25 = 2.75


这里比较有意思的一点是:

十进制小数点后面的那一位(也就是十分位), 对应的是1/10, 也就是0.1

即, 对于十进制数3.4, 这个4对应的值是: 4 * 权  =  4 * 0.1 = 0.4

而二进制小数点后面的一位, 对应的是1/2, 也就是十进制的0.5

所以对于二进制数0.1, 这个1对应的值是: 1 * 0.5 = 0.5, 所以二进制的0.1, 其实等于十进制的0.5

这让我想起来一个脑筋急转弯, 问: 什么时候 1.11.3 要大?

答: 当1.1是个二进制数, 而1.3是个十进制数的时候...


事实上: 对于小数点之后的位, 二进制的位权始终比十进制的位权要大, 举例:

十进制数: 小数点之后的位权依次是: 1/10,  1/100,  1/1000...

二进制数: 小数点之后的位权依次是: 1/2,  1/4,  1/8...  相应位的权始终比↑十进制的要大

所以会出现这种现象

二进制:                1.000001, 小数点后面的数看起来已经很小很小了

对应的十进制是:  1.015625, 小数点后面的数其实还挺大...


在IEEE765标准中, 我们会经常和二进制小数打交道, 所以这里补充一下相关知识.


三. 关于32位浮点数, 一些不太正确的认知

1. 32位浮点数能存储很大的整数

这是32位浮点数的取值范围:

当我第一次看到这个取值范围时, 我是很惊讶的, 怎么这么大?

一个浮点数, 占用32字节, 竟然能存储下约±340000000000000000000000000000000000000这么大的数

相比之下, 一个同样32字节的long类型, 存储范围只有约±2147483647

那我为啥还要用long类型...

...

一路学习到现在, 倒是可以绕过这个弯儿了, 那就是:

32位浮点型确实最大可以存储到这么大的数, 但精度很低

第三章中我们说过, 32位浮点数表盘中的蓝点会越来越稀疏:


等到了这么大的数时, 其实蓝点已经稀疏的不成样子了, 基本是不可用状态

根据wiki中给出的间隔, 对于1.70141e38 到 3.40282e38范围中的数, 间隔是2.02824e31

也就是说, 大体上: 32位浮点数中, 能精确存储1.70141e38

但无法精确存储1.70141e38 + 1

也无法精确存储1.70141e38 + 2,

也无法精确存储1.70141e38 + 100000000000

...

下一个能精确存储的数是: 1.70141e38 + 20282400000000000000000000000000 (即加上间隔)

这个精度基本上是不可用的.

事实上, 如果你要用float存储整数的话, 最多只能精确存储到 16777216

再大的话, 间隔就会变为2, 就不适合用来存储整数了:

此时再回过头来看看同为 32位 的long类型, 能精确存储的整数范围

约是: ±2147483647

比:    ±16777216    大多了

所以存储大整数还是用long类型吧

总结: 32位浮点数只是有能力存储到, 实际上存储的数过大会导致精度过低, 基本上不可用. 用32位浮点数存储整数时, 只适用存储±16777216之间的整数.


2. 32位浮点数能存储很精确的小数

这是32位浮点数的取值范围:

看起来好像能存储这么精确的小数...

但其实和存储整数一样, 32位浮点数只是有能力存储到这么小的小数而已...

事实上在第三章中我们详细讲解过: 32位浮点数的精确度是7位有效数.

即如果你要存储的数 整数部分 + 小数部分 放在一起超过了 7 位, 32位浮点数就不能精确存储了

比如, 32位浮点数就不能精确存储我们常背的部分圆周率

32位浮点数倒是可以存储常见的月工资, 比如 5078.65, 或 12665.73. 但如果要存储年工资, 或把工资存储到3位小数, 32位浮点数就不一定够用了...

所以, 虽然32位浮点数的取值范围看起来很大, 足足有:

但其实32位浮点数只适合存储常见数据...

感性地去认知的话, float(也就是32位浮点数)类型其实和int类型有些相似: int用于存储最常用, 最自然的整数. float则用于存储最常用, 最自然的浮点数...编程时, 如果要存储的数很大或精度很高(相对来说,这些数往往不怎么常用或不怎么自然), 就要考虑改用long或double.

精确来说的话, 就是不要被32位浮点数骇人的取值范围吓到. 而是记住事实上它只能存储7位有效数就行了.

你可能感兴趣的:(IEEE 754规范: 五, 一些补充)