完全解析为什么0.1+0.2=0.30000000000000004

阅读全文根据基础需要20-60分钟

引子

可能因为某一次报错或某次面试,你会接触到这个问题,即
在这里插入图片描述
其实该问题很多语言都存在,你也可以在https://0.30000000000000004.com/了解其他信息,本文主要以Java为例对该问题进行讨论

计算结果多样原因

如果你已经参阅了好几篇讲解0.1+0.2为什么!=0.3的文章,你会发现大家在浮点数的讲解上大同小异,不过最终0.1+0.2的结果却大体分为4种不同的结果,这里的原因是

  • 有些博主以float(单精度浮点数)举例
  • 有些博主以double(双精度浮点数)举例

同时在计算过程中,博主们的计算过程大致为为两类

  • 将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制
  • 将0.1与0.2转为实际的二进制浮点数 → 将浮点数转回十进制 → 直接进行十进制数计算

所以大家会看到4种不同的计算结果,如果除此4种结果以外还存在其他结果,那么多半是由于粗心在某些步骤出现了计算失误,本篇会带着大家逐步完成其中3种情况的计算,最后一种留作习题用以检验自己的学习成果

需要注意的是,Java本身的计算顺序是
将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制
所以其他3种情况的结果都不和0.30000000000000004相符

正文

Java中的默认数据类型

我们首先需要知道,Java中的整数默认是int型,小数默认是Double
所以我们可以理解为这是Double型的0.1与0.2相加

到底什么是Double?

Double是符合IEEE_754-1985计算机浮点数标准的双精度浮点数,这一标准也规定了浮点数要以什么样的形式保存在计算机中

10进制转换为浮点数

在 IEEE_754-1985 规范中规定了浮点数的二进制表现形式以及长度限制
长度限制如下

数据类型定义长度
float单精度浮点数32位2进制
double双精度浮点数64位2进制

浮点数的表现形式分为3部分,以上题需求,这里以double为例
在这里插入图片描述

  • 第一部分为符号位,仅占用1位(bit)——0为正,1为负
  • 第二部分为指数位,占用11位,用来存储经偏指数进行计算过后的指数
  • 第三部分为分数部分,占用52位,用来存储分数,分数长度不够后方全部补0直到长度符合要求

下面我们以十进制数0.15625为例,逐步计算将其转为双精度浮点数,来让大家实际了解十进制数如何转为双精度浮点数

符号位

0.15625 很明显是正数,所以符号位为0

指数位

这里我们需要先将十进制数0.15625转为二进制数,即为0.00101

如果你对2进制不了解,可以百度搜索进制转换工具帮助完成这一步,这里不做进制转换工具推荐

之后我们需要以科学计数法的形式表示0.00101,即为1.01 * 2 指数为-3

科学计数法:
把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数
举例
12的科学计数法表示为:1.2 * 10 指数为1
在二进制中即为把一个数表示成a与2的n次幂相乘的形式(1≤|a|<2,a不为分数形式,n为整数

同时浮点数标准也为不同精度的浮点数规定了固定的指数位偏差,双精度浮点数(double)的固定偏差为1023
此时计算指数与偏差,即(1023) + (-3) = 1020,将1020转为二进制,即为1111111100
此时我们回看指数位的定义,double的指数位长度为11位,(1111111100)长度为10位,所以我们需要在数前补0直到长度为11位,即将1111111100变换为01111111100
所以指数位为 01111111100

分数位

回看上一步得到的科学计数法的表示——1.01 * 2 指数为-3
分数部分即为小数部分 即为1.01中的01,不过由于01的位数太少,我们需要在其后方补0直到满足分数位52位的要求
结果为:0100000000000000000000000000000000000000000000000000
最终进行拼接,即0.15625的双精度浮点数表示为
0011111111000100000000000000000000000000000000000000000000000000

浮点数转换为10进制

大家已经会了10进制如何转为浮点数了,那么大部分人应该已经能推导出浮点数要如何转回10进制了,不过这里依然还是要讲一下
还是以
0011111111000100000000000000000000000000000000000000000000000000为例
首先区分3部分
0 01111111100 0100000000000000000000000000000000000000000000000000
符号位可以确定数为正数,指数位部分2进制转10进制,为1020,double偏指数为1023
1020 - 1023 = -3则指数为-3
分数部分可以简写为01,需要注意的是,这里隐含的前导数固定为1,即可以知道原数为1.01
此时进行计算1.01 * 2 指数为-3,得出结果为0.00101,最后直接2进制转回10进制即可

Double——情况1讨论

这里的情况为,将小数类型以Double处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制 进行处理

0.1的二进制浮点数

0.1的二进制浮点数为
0011111110111001100110011001100110011001100110011001100110011010
如果你以上方讲解进行转换,会遇到无限循环小数问题,即0.1转为2进制是无限循环小数,这时大家得出的结果应该都为
0011111110111001100110011001100110011001100110011001100110011001
那么为什么这里我的结果和大家不一样呢?因为当分数部分位数不够,即出现分数丢失精度情况时,我们需要做一个操作——舍入
浮点数的默认舍入方式和大家平时理解的四舍五入基本一致,在本情况下直接将最后一位1向前进位1即可

如果你希望对浮点数的舍入进行更多了解,可以自行百度
0.2的二进制浮点数

0.2的二进制浮点数为
0011111111001001100110011001100110011001100110011001100110011010
0.2同样存在舍入操作

浮点数计算

此时我们需要计算
0.1 → 0011111110111001100110011001100110011001100110011001100110011010
+
0.2 → 0011111111001001100110011001100110011001100110011001100110011010
的结果,不过浮点数运算和常规运算不同,我们一步一步进行

对阶——可能影响数据准确

浮点数计算的第一步为对阶,即将参与计算的浮点数的指数位保持一致
需要注意,对阶的原则为小阶向大阶对阶
这里我们首先计算他们的阶码
0.1阶码 → 01111111011 → 1019
0.2阶码 → 01111111100 → 1020
所以我们这里需要做的就是将0.1的阶码+1变为1020
01111111011 → 01111111100
因为阶码的改变,为了不让浮点数表示的数发生改变,分数部分需要向右同步移1位,这里需要注意,分数部分隐含前导数为1,即右移的第一位进来的数是1

如果这一步你没看懂,可以以科学计数法的形式进行思考,即
12 → 1.2 * 10 阶码为1
此时需要将阶码改为2,则分数部分需要进行变动,最终表示变为
0.12 * 10 阶码为2

1001100110011001100110011001100110011001100110011010 变为
1100110011001100110011001100110011001100110011001101
对阶结束时,0.1和0.2的值分别为
0.1 → 0011111111001100110011001100110011001100110011001100110011001101
0.2 → 0011111111001001100110011001100110011001100110011001100110011010

尾数计算(分数计算)

浮点数计算的第二步为直接计算浮点数的第三部分,这里需要注意不要丢失分数部分隐含的前导数
0.1 → 0.1100110011001100110011001100110011001100110011001101
+
0.2 → 1.1001100110011001100110011001100110011001100110011010
=
0.3 →10.0110011001100110011001100110011001100110011001100111
尾数部分计算到这里就结束了,我们接着下一步处理

这里0.1的前导数为0是因为分数部分在对阶的阶段已经右移了1位所以隐含前导数1已经移动到了分数部分的开头
规格化处理——可能影响数据准确

IEEE 754-1985标准要求浮点数的隐含前导数必须为1,但是此时计算结果为
10.0110011001100110011001100110011001100110011001100111
隐含前导数为10,所以我们需要进行规格化处理,让前导数重新变为1
这里我们需要执行的操作为右规,即将分数部分向右移动一位
10.0110011001100110011001100110011001100110011001100111 变为
1.0011001100110011001100110011001100110011001100110011
需要注意,这里最后一位的1被丢掉了,从而会影响数据准确
规格化处理后的分数部分为
0011001100110011001100110011001100110011001100110011
因为分数部分向右移动了一位,所以阶码需要+1
01111111100 → 01111111101

规格化处理分为左规和右规,左规即将分数部分向左移动,右规即将分数部分向右移动
舍入操作

由于规格化处理时导致低位丢失,所以需要进行舍入操作,即
0011001100110011001100110011001100110011001100110011 变为
0011001100110011001100110011001100110011001100110100

溢出判断

这里没有溢出

最终计算结果即为
符号位 0
指数位 01111111101
分数 0011001100110011001100110011001100110011001100110011

结果转为10进制

01111111101 代表指数为1021 带入偏差1023进行计算结果为-2
所以分数小数点需向左移动2位
0011001100110011001100110011001100110011001100110011 变为
0.010011001100110011001100110011001100110011001100110011
转为10进制为:3.00000000000000044408920985006E-1
即 0.300000000000000044408920985006
这就是为什么0.1+0.2=0.30000000000000004的原因

Double——情况2讨论

这里的情况为,将小数类型以Double处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 将浮点数转回十进制 → 直接进行十进制数计算 进行处理
0.1和0.2的浮点数可以在上种情况中直接拿来使用
0.1 → 0011111110111001100110011001100110011001100110011001100110011010
0.2 → 0011111111001001100110011001100110011001100110011001100110011010

浮点数转回10进制

0011111110111001100110011001100110011001100110011001100110011010 → 0.100000000000000005551115123126
0011111111001001100110011001100110011001100110011001100110011010 → 0.200000000000000011102230246252

10进制计算

0.100000000000000005551115123126
0.200000000000000011102230246252
0.300000000000000016653345369378
因此最终结果为0.300000000000000016653345369378

Float——情况1讨论

这里的情况是,将小数类型以Float处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 将浮点数转回十进制 → 直接进行十进制数计算 进行处理
0.1 → 00111101110011001100110011001101
0.2 → 00111110010011001100110011001101

浮点数转回十进制

00111101110011001100110011001101 → 0.100000001490116119384765625
00111110010011001100110011001101 → 0.20000000298023223876953125

10进制计算

0.100000001490116119384765625
0.20000000298023223876953125
0.300000004470348358154296975
因此结果为0.300000004470348358154296975

到此为止我说的4种情况其中的3种都进行逐步计算,最后一种的情况为

将小数类型以Float处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制 进行处理

这种情况留作检验阅读本博客的作业,大家可以自己尝试计算一下

参考文章

  1. IEEE 754-1985
  2. Floating Point Math
  3. 浮点数转换工具网站

最后更新于2022年3月14日
原创不易,如果该文章对你有所帮助,望左上角点击关注~如有任何技术相关问题,可通过评论联系我讨论,我会在力所能及之内进行相应回复以及开单章解决该问题.

该文章如有任何错误请在评论中指出,感激不尽,转载请附出处!
个人博客首页:https://blog.csdn.net/yjrguxing ——您的每个关注和评论都对我意义重大

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值