为什么在处理金额时不能用Double和Float,计算机是如何存储浮点数的

前言

码出高效这本书以前翻到浮点数这一页的时候随便看一下过了就过了,最近再次开始翻,才发现浮点数这个东西其实并没有想象中这么简单,于是写一篇文章记录一下。

想要弄懂浮点数的存储方式,要先有三个前置技能点,科学计数法,进制转换和移码,我会在本文一起介绍这三点。

1. 十进制和二进制的转换

在介绍如何存储之前,我们要先对进制转换有一个比较清晰的认知,当然这里所说的进制转换主要是要介绍小数的进制转换,整数部分就不介绍了。

1.1 十进制转二进制

方法:将小数乘以2,取出整数部分作为二进制的第一位,小数部分继续乘以2,再次取出整数部分作为第二位;反复执行这个操作直到小数部分为0。

举例1,0.625转换成二进制:

0.625 * 2 = 1.25      1
0.25  * 2 = 0.50      0
0.50  * 2 = 1.00      1

所以0.625转为二进制为0.101

举例2,0.6转换成二进制:

0.6 * 2 = 1.2       1
0.2 * 2 = 0.4       0
0.4 * 2 = 0.8       0
0.8 * 2 = 1.6       1
0.6 * 2 = 1.2       1
....
你会发现对于小数0.6而言,他是永远都乘不尽的,会出现无限循环小数的情况。
所以0.6转为二进制为0.1001  1001  1001 .......

1.2 二进制转十进制

方法:二进制中每一个位数的值乘以2的X次方,X为位数所在的位置,小数点前一位(个位数)的位置为0。

举例1,1011.101转为十进制:
= 1 * 23 + 0 * 22 + 1 * 21 + 1 * 20 + 1 * 2-1 + 0 * 2-2 + 1 * 2-3
= 8 + 0 + 2 + 1 + 0.5 + 0 + 0.125
= 11.625。

举例2,0.1001 1转为十进制:
= 1 * 2-1 + 0 * 2-2 + 0 * 2-3 + 1 * 2-4 + 1 * 2-5
= 0.5 + 0 + 0 + 0.0625 + 0.03125
= 0.59375。
你会发现0.6转为二进制后,再转回十进制,是永远得不到0.6的值,只能无限接近(例子只取了前五位)。

2. 科学计数法

在日常生活中,如果我们需要记一个非常大或者非常小的数字,经常会使用科学计数法来表示,比如说光的速度近似于3.0 * 108 m/s;我们国家人口大概有1.4 * 109人。科学计数法可以表达方式如图所示:
在这里插入图片描述

  1. 有效数字部分的大小为:1<= x < 10。他是一个不能大于等于10的数,如果有效数字大于10的话,则表示形式为小数点前移一位,指数+1;他是一个不能小于1的数,如果小于1的话,表示形式为小数点后移一位,指数-1。
  2. 底数固定为10,不能更改。

这里要注意,我们说的科学计数法,其实是十进制的科学计数法,但是对于机器而言,它是不认我们十进制的,它只认二进制。那么,如果要将我们的十进制的科学计数法,运用到机器上,那该怎么做呢?答案很简单,把科学计数法改编成二进制的就好。
在这里插入图片描述

  1. 有效数字部分的大小为:1<= x < 10(二进制,也就是十进制的2),它只能无限接近2但是却不能等于。且由于是二进制,所以有效数字的整数部分永远为1
  2. 底数固定为10(二进制,也就是十进制的2),不能更改。

这样一来,我们的科学计数法在计算机上也可以正常使用了。虽然我在图例中都是取某个很大的数,但是不要忘记,指数部分的取值是可以小于0的,这样一来。就可以表示一个小于1的很小的数了。

3. 浮点数格式

讲完这两部分,就可以介绍浮点数的表示方式了,没有错,浮点数的表示正是用我们的科学计数法,看图(单精度为例)。
在这里插入图片描述
图中的符号,指数和有效数字,直接对应科学计数法中的符号,指数和有效数字,一一对应。

注意一下,由于考虑到现实世界中不同机器的实现方式,所以称谓会有所改变。指数称为"阶码",有效数字称为"尾数",知道两者是同一事物即可。

  • 符号位:用于存储浮点数的符号,0是正数1是负数。
  • 阶码位(指数):存储科学计数法中的指数部分,此处要注意的是存储方式是通过移码的方式存储,而非我们所熟知的补码。
  • 尾数位(有效数字):以十进制直接转为二进制的方式(也就是原码),存储有效数字的小数部分。上文提到过,由于是二进制,所以整数部分的数一定是1,就没有必要存储这个1了,只存储小数部分即可。

3.1 为什么不能用浮点数存储金额

通过刚刚二进制和十进制互相转换的例子,就能很清楚的知道,对于一部分二进制数,他的精度是有缺失的。
就以刚刚的0.6来说,当转成二进制存储时,它就已经精度缺失了,再转回来只能无限接近0.6却不能等于。虽然实际上浮点数是通过科学计数法来存储的,但是精度缺失这个特性并不会消失,因为有效数字依然有小数部分。

虽然不是这篇文章的内容,但是也提一下,在Java中,用于金额这种需要准确计算时,一般用BigDemical类,这个类为什么可以做到浮点数都做不到的精确计算呢,因为它内部的实现方式,是通过字符串的来精准的存储每一位数,自然就不会出现精度缺失的情况。

但是要注意,构造BigDemical类的时候,不要直接传double或者float,这样数据一开始就会出错,找个地方运行一下如下代码就清楚了。

public class Tem {
    public static void main(String[] args) {
        BigDecimal bigDecimal = new BigDecimal(3.51);
        System.out.println(bigDecimal); // 3.5099999999999997868371792719699442386627197265625
        BigDecimal bigDecimal1 = new BigDecimal("3.51");
        System.out.println(bigDecimal1); // 3.51
    }
}

4. 移码

最后再来说一下移码,移码是专门用于表示浮点数中阶码位部分(也就是指数部分)的值,具体的表示方式为:
[X]移 = 2n-1 + X(-2n-1 <= X < 2n-1 ),其中X表示我们数字的真值,n表示二进制位数。

我举个例子,假设我们用八位二进制数,那么:
对于数字5
原码:0000 0101
反码:0000 0101
补码:0000 0101
移码为 5 + 28-1 = 5 + 128 = 133,移码的值直接转为二进制
移码:1000 0101

对于数字-5
原码:1000 0101
反码:1111 1010
补码:1111 1011
移码为 -5 + 28-1 = -5 + 128 = 123,移码的值直接转为二进制
移码:0111 1011

上述两个例子可以看出来,移码和补码的区别就是符号位相反,
我们在计算移码时,所增加的2n-1(也就是例子中的128)被称为偏移量

4.1 机器中所使用的偏移量

上文是标准情况下的移码的计算方式,但是在IEEE754标准(简单来说就是浮点数计算标准)中,它所规定的偏移量不是一般情况下移码所用的2n-1,而是2n-1 - 1(也就是127),我们的计算机一般都采用这个标准。

所以在IEEE754标准下,
对于数字5
原码:0000 0101
由于它已经不是我们所熟知的移码了,就直接用阶码来称呼它
阶码为 5 + (28-1 - 1) = 5 + 127 = 132
阶码:1000 0100

对于数字-5
原码:1000 0101
补码:1111 1011
阶码为-5 + (28-1 - 1) = -5 + 127 = 122
阶码:0111 1010

4.2 为什么要使用移码

但是光是知道移码是什么还不够,我们还需要知道为什么浮点数需要使用移码而不是平时所用的补码。

首先移码的八位数是不带符号位的,所以移码的取值区间为[0,255],而我们平时所用的补码,它是带符号位的,取值区间为[-128,127]。移码所做的就是,将所有的补码,映射成一个正数,这样在进行数字大小的比较时就会比较方便,不用考虑符号的因素。

由于浮点数计算是计算机的底层计算,所以它需要尽可能的方便和快速,所以这就是浮点数要使用移码的原因。

4.3 为什么不是2n-1

首先要在这里加一个设定,阶码的取值范围为[0,255],但是在阶码中,全0代表着机器零,全1代表着无穷大,在去掉这两个数之后,阶码的取值范围就变成了[1,254],如果还按照着2n-1(128)的偏移量,所对应的实数范围就为[-127,126],最大值就只能取到126。

回到浮点数的表达公式,最大126就表示你的浮点数最大只能乘以2126,为了能够让浮点数尽可能表达更大的数字,所以采用2n-1-1(127),这样对应的实数范围就为[-126,127],浮点数最大可以乘以2127,浮点数的取值区间翻倍。

有些人可能会有疑问,那为什么不用128,这样就可以表达更小的数字了,我只能说,一般情况下小数点后四五位都不一定用得上,要那么小的数并不会特别实用,所以用127而非用128。


机器零:想必你已经发现了,就算是浮点数,受限于它的位长,也不可能表达一个无穷小的数字,最小的数字为1.0 * 2-126 。由于无穷小无法到达,就有必要单独拿一个数出来记为0,这个就是机器零。
而机器零也不仅仅代表0这个数,在浮点数中机器零代表了0和其附近的一片超越浮点数精度的区域。
无穷大:表示数字太大超过了浮点数的表达范围,浮点数上溢。

机器零_百度百科
https://baike.baidu.com/item/%E6%9C%BA%E5%99%A8%E9%9B%B6

参考材料

码出高效:Java开发手册
p8 - p10
小数用二进制如何表示_Lightmare625-CSDN博客_二进制表示小数
https://blog.csdn.net/weixin_41042404/article/details/81276782
汇编学习笔记之阶码与移码_fancy_track的博客-CSDN博客_阶码 移码
https://blog.csdn.net/fancy_track/article/details/79728311
移码_百度百科
https://baike.baidu.com/item/%E7%A7%BB%E7%A0%81/10165919?fr=aladdin

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值