本章讨论一些零碎的话题.
一. 其他导致浮点数存储不精确的原因
在第三章中我们提到过, 数学中的小数是连续的, 而计算机中的小数(准确来说是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.1 比 1.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位有效数就行了.