为什么0.1 + 0.2不等于0.3?一次讲透计算机的数学“Bug”

2025-12-04 0 854

01 引言

0.1+0.2≠0.3这样问题在程序界,可能大加都知道结论,也可能知道如何规避。但是到底是什么原因呢?

当然,我们都知道这是计算机底层二进制的问题。但是为何会这样呢?我们一起看看吧。

02 情景复现

2.1 JavaScript

在任意的浏览器,调出开发者模式,在控制台输入:

console.info(0.1+0.2);
console.info(0.3);
console.info(0.1+0.2 == 0.3);

下图为谷歌浏览器案例:

我们发现0.1+0.2 == 0.3返回的结果是false。而0.1+0.2=0.300000000000000040.3确实不相等。

2.2 Java

@Test
void test01(){
    System.out.println(0.1 + 0.2);
    System.out.println(0.3);
    System.out.println(0.1 + 0.2 == 0.3);
    System.out.println(\"---------------------------------\");
    System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
    System.out.println(new BigDecimal(0.3));
    System.out.println(\"---------------------------------\");
    System.out.println(new BigDecimal(\"0.1\").add(new BigDecimal(\"0.2\")));
    System.out.println(new BigDecimal(\"0.3\"));
}

基本数据类型和BigDecimal的运算都会出现0.1+0.2≠0.3的问题。

运行结果:

03 原因分析

案例中直接打印,没有问题是因为没有参与二进制位的运算,所以不会有精度的丢失。

要了解精度问题,就要知道在计算机中如何存储二进制位的。案例中的小数为float类型,占4个字节,32位。

3.1 二进制标准

我们知道数学界有无限循环小数,也有无限不循环小数。而计算机要处理写小数,也有统一的国际标准:IEEE 754

它规定了计算机如何用二进制来表示和计算小数(浮点数)。它就像一套世界通用的“小数书写法则”,确保了在不同计算机上,同一个小数能有相同的表示,并且计算结果是可预测的。

在 IEEE 754 出现之前,不同厂商的计算机可能用不同的方式表示小数,导致程序在一台机器上运行正常,在另一台机器上结果却不一样。IEEE 754 统一了这个规则,解决了可移植性和可靠性的问题。而IEEE 754的核心思想就是科学计数法,只不过是二进制中的科学技术法。

float的32位如何划分:

部分 符号 指数 尾数
比特数 1 bit 8 bits 23 bits

符号

占1个bit,0 表示正数,1 表示负数。也就我们常说的符号位。

指数

占8个bit,可以标识0~255。为了能表示负指数(比如 2⁻²),IEEE 754 使用了一个“偏置值”。对于单精度,这个值是 127。而指数位(编码指数)的计算是有公式的:编码指数 = 实际指数 + 127

尾数

占23个bit,指的是有效数字的小数部分。在二进制科学计数法中,任何一个数(除了0)都可以表示为 1.xxxxx × 2^指数。注意,开头的那个 1 是固定的,所以为了节省一个比特,IEEE 754 规定这个 1 是“隐藏的”,不需要存储。我们只需要存储后面的 xxxxx(小数部分)即可。这叫做“隐藏位技术”

3.2 二进制位转化

我们以0.1的二进制位为例:

0.1 * 2 = 0.2 -> 0
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.2 * 2 = 0.4 -> 0 (从这里开始循环)

二进制位就是将整数位以此拼接。

所以0.1的二进制是:0.000110011001100110011001100110011… (循环节0011)

使用科学计数法(规格化),小数点需要香油移动4位,所以:0.1 = 1.10011001100110011001100... × 2^(-4)。这里的-4就是实际的指数位。那么计算编码指数:编码指数 = -4 + 127 = 123。转化成二进制就是01111011

尾数是23位。只需要从1.10011001100110011001100...× 2^(-4)小数点,向后截取24位。多处的以为是直接舍去还是进一,是有逻辑处理的。处理之后,最后保留23位。

10011001 10011001 1001100 1...第24位是1,后面还有,所以粘滞位为1。根据向最接近的偶数舍入(默认舍入模式):第24位是1,且后面有非零位,所以需要向上舍入(即第23位加1)。所以,原来的23位是:10011001100110011001100
向上舍入后变为:10011001100110011001101

因此,0.1的单精度浮点数表示为:
符号位:0
指数位:01111011
尾数位:10011001100110011001101
组合起来:0 01111011 10011001100110011001101

可视化展示:

31  30    23  22                                      0
┌───┬─────────┬───────────────────────────────────────┐
│ 0 │ 01111011│ 10011001100110011001101               │
└───┴─────────┴───────────────────────────────────────┘
 │     │                     │
 │     │                     └── 尾数域 (23 bits)
 │     │                         - 存储规格化后的小数部分
 │     │                         - 隐藏位为1(不存储)
 │     │
 │     └── 指数域 (8 bits)
 │         - 编码值: 123 (二进制 01111011)
 │         - 实际指数: 123 - 127 = -4
 │
 └── 符号位 (1 bit)
     - 0 表示正数

让我们验证这个位图确实表示约等于 0.1 的值:

  • 符号 = (-1)⁰ = +1
  • 指数 = 2⁻⁴ = 1/16
  • 尾数 = 1 + (1×2⁻¹ + 0×2⁻² + 0×2⁻³ + 1×2⁻⁴ + …)

计算尾数的十进制值:

1.10011001100110011001101₂ = 
1 × 1 + 
1 × 0.5 + 
0 × 0.25 + 
0 × 0.125 + 
1 × 0.0625 + 
1 × 0.03125 + 
0 × 0.015625 + 
0 × 0.0078125 + 
1 × 0.00390625 + 
1 × 0.001953125 + 
... (继续所有23位)

最终结果 ≈ 1.600000023841858
乘以指数部分:1.600000023841858 × 2⁻⁴ = 0.10000000149011612

3.3 位计算

由上面的计算0.1=1.10011001100110011001101₂,实际指数为-4

同理可以算出:0.2 = 1.10011001100110011001101₂,实际指数为-3

将实际指数调整相同:

0.1 = 0.110011001100110011001101 × 2⁻³

0.2 = 1.10011001100110011001101 × 2⁻³

计算:

text

  0.110011001100110011001101  (调整后的0.1)
+ 1.10011001100110011001101   (0.2)
─────────────────────────────
 10.011001100110011001100111

规格化处理后:1.0011001100110011001100111 × 2⁻²

最终舍入处理得到:1.00110011001100110011010 × 2⁻²

验证结果:

1.00110011001100110011010₂ = 
1 × 1 + 
0 × 0.5 + 
0 × 0.25 + 
1 × 0.125 + 
1 × 0.0625 + 
0 × 0.03125 + 
0 × 0.015625 + 
1 × 0.0078125 + 
1 × 0.00390625 + 
0 × 0.001953125 + 
... (继续所有23位)

最终结果 ≈ 1.199999928474426269531250000
乘以指数部分:1.199999928474426269531250000 × 2⁻² = 0.299999982118606567382812500

而0.3在计算机的存储:0.300000011920928955078125。所以值会不相等。

可以通过这个转化工具直接查看:

地址:www.h-schmidt.net/FloatConver…

04 小结

计算机底层的东西比较晦涩难懂,如有问题还挺多多包含。上面的Java案例中BigDecimal不同的构造,保存的值不一样。

@Test
void test02(){
    System.out.println(new BigDecimal(\"0.3\"));
    // 0.3

    System.out.println(new BigDecimal(0.3));
    // 0.299999999999999988897769753748434595763683319091796875
}

所以使用的BigDecimal构造的时候需要慎重,尽量使用字符串参数的构造函数。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 为什么0.1 + 0.2不等于0.3?一次讲透计算机的数学“Bug” https://www.zuozi.net/3594.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务