为什么0.1 + 0.2 不等于 0.3 ?

简介: 这篇编程技术文章探讨了为什么在多种编程语言中,0.1 + 0.2 不等于 0.3 的现象。问题源于计算机使用二进制浮点数表示小数,而二进制无法精确表示某些十进制分数,如 0.1 和 0.2。这导致它们在计算机内部被近似表示,从而在相加时产生微小误差。文章通过示例和图片解释了二进制浮点数的表示原理,并提供了将小数转换为整数再相加以及使用 `toFixed()` 方法或 decimal.js 库等解决精度问题的方法。


在很多编程语言中,我们都会发现一个奇怪的现象,就是计算 0.1 + 0.2,它得到的结果并不是 0.3,比如 C、C++、JavaScript 、Python、Java、Ruby 等,都会有这个问题。

为此,有人还做出了一个 https://0.30000000000000004.com/ 网站,在这个网站上我们可以找到哪些编程语言在计算 0.1 + 0.2 的结果并不是 0.3,而是 0.30000000000000004。

我们可以用 JavaScript 来做演示,计算 0.1 + 0.2,它得到的结果并不是0.3,而是 0.30000000000000004。

当然如果你把它作为条件判断语句,返回的也是 false:

还有其他的例子:

6 * 0.1 = 0.6 但计算机显示为 0.6000000000000001

0.11 + 0.12 = 0.23 但计算机显示为 0.22999999999999998

0.1 + 0.7 = 0.8 但计算机显示为 0.7999999999999999

0.3+0.6 = 0.9 但计算机显示为0.8999999999999999

而以下几个计算式却能得到我们想要的结果:

这是为什么呢?

简单来说,计算机使用基于二进制的浮点数,而我们人类使用基于十进制的浮点数。

在二进制中,,浮点数通常使用 IEEE 754 标准进行表示,无法准确表示的小数有 0.1、0.2 或 0.3 这样的数字,因为它使用的是二进制浮点格式。

IEEE 754 标准可以参见:https://zh.wikipedia.org/wiki/IEEE_754

背后原理

在十进制系统中,如果一个分数使用基数(10)的质因数来表示,那么它可以被精确地表示。

10 的质因数是 2 和 5。

因此,1/2、1/4、1/5 (0.2)、1/8 和 1/10 (0.1) 可以被精确地表示,因为分母使用了 10 的质因数。

而 1/3、1/6 和 1/7 是无限循环的小数,因为分母使用了 3 或 7 的质因数。

在二进制(计算机使用的系统)中,如果一个分数使用基数(2)的质因数来表示,那么它可以被精确地表示。

2 是 2 的唯一质因数。

因此,1/2、1/4 和 1/8 都可以被精确地表示,因为分母使用了 2 的质因数。

而 1/5 (0.2) 或 1/10 (0.1) 是无限循环的小数,因为分母使用了 5 或 10 的质因数。

所以当我们尝试表示像 0.1 这样的十进制小数时,计算机会使用一个近似值。这个近似值是通过将无限循环的二进制小数转换为有限位数的浮点数表示来实现的。

因此,当我们在计算机中进行浮点数运算时,结果可能会有微小的误差。

例如,0.1 在二进制中的近似表示可能是 0.000110011001100...,但在计算机的浮点数表示中,它可能被截断或舍入为 0.00011001100110,这就导致了 0.1 + 0.2 在计算机中可能不等于 0.3,而是略微有所偏差。

0.1 是十分之一(1/10),要得到 0.1 的二进制表示(即二进制形式),我们需要使用二进制长除法,即将二进制数1除以二进制的 1010(即1/1010),如下所示:

因此,0.1 在二进制中的表示为 0.0001100110011001100110011...(无限循环)。

这个无限循环的模式 0011 会一直重复下去,因为二进制系统只能通过这种方式来近似表示十进制中的 0.1。

在实际的计算机系统中,这个无限循环的小数会被截断为有限位数,以便存储和计算。这就导致了在计算机中进行二进制浮点数运算时,可能会出现精度损失,从而使得 0.1 和 0.2 的和不完全等于0.3。

十进制小数转二进制

还有一种更容易理解的方法(采用 *2 取整法),例如我们要把十进制数的小数 0.875 转换为二进制数,只需将十进制数的小数部分乘以 2,然后提取整数部分,直到小数部分变为 0。

将上面提取的整数部分排列的结果 111 变成以二进制表示的 .875

二进制数 1101.111 整数部分为 1101 ,小数部分为 111,就是十进制数 13.875 转换为二进制的结果。

按以上的方法我们将十进制小数 0.1 转化为二进制就是:

0.1 = 0.0001100110011001100110011001100110011001100110011001101...

十进制小数 0.2 转化为二进制就是:

0.2 = 0.001100110011001100110011001100110011001100110011001101...


解决办法

1、小数先转整数

可以先把小数转换成整数,再相加之后转回小数,如下实例:

(0.1*10 + 0.2*10)/10

2、使用 toFixed() 方法

toFixed() 方法可以将一个数字转换为指定小数位数的字符串表示形式。

下面是一个使用 toFixed() 方法解决浮点数精度问题的例子:

let sum = 0.1 + 0.2;

console.log(sum.toFixed(2)); // 输出: 0.30

需要注意的是,toFixed()方法虽然在显示上解决了问题,但它并没有改变数字的实际值,它只是改变了数字的表示形式。

如果你需要进行精确的数学运算,可能需要使用其他方法,比如引入一个精度更高的数值类型或者使用第三方的数学库来处理浮点数运算。

3、使用 decimal.js 库

在 JavaScript 中处理浮点数的精度问题时,使用 decimal.js 库是一个更为精确和可靠的解决方案。

decimal.js 是一个任意精度的十进制数学库,它能够避免原生 JavaScript 中浮点数运算的不精确性。

Github 地址:https://github.com/MikeMcl/decimal.js

首先,你需要在你的项目中引入 decimal.js 库。

使用 npm 安装:

npm install decimal.js

在 HTML 中引入:

<script src="https://cdnjs.cloudflare.com/ajax/libs/decimal.js/10.2.0/decimal.min.js"></script>

接下来,你可以使用 decimal.js 来处理浮点数的运算,下面是一个例子:

实例

// 引入Decimal构造函数

const Decimal = decimal. Decimal;


// 创建两个Decimal对象

let a = new Decimal('0.1');

let b = new Decimal('0.2');


// 进行加法运算

let sum = a.plus(b);


// 输出结果

console.log(sum.toString()); // 输出: 0.3

使用 decimal.js 库得到的结果是准确的 0.3,而不是原生 JavaScript 中的近似值。

相关文章
|
机器学习/深度学习 算法 数据可视化
〖数据挖掘〗weka3.8.6的安装与使用(1)
Weka的全名是怀卡托智能分析环境(Waikato Environment for Knowledge Analysis),是一款免费 的,非商业化软件,与之对应的是SPSS公司商业数据挖掘产品--Clementine ;weka是基于JAVA环境下开源的 机器学习(machine learning)以及数据挖掘(data mining)软件。Weka的主要开发者来自新西兰的怀卡托大学(The University of Waikato)。WEKA作为一个公开的数据挖掘工作平台,集合了大量能承担数据挖掘任务的机器学习算法,包括对数据进行预处理,分类,回归、聚类、关联规则以及在新的交互式界面上的
713 0
|
JavaScript 前端开发
Vue3-v-bind事件绑定
Vue3-v-bind事件绑定
578 0
|
弹性计算 Java Linux
手把手教你把Springboot项目部署到阿里云教程
手把手教你把Springboot项目部署到阿里云教程成功连接远程云服务器和配置阿里云安全组
2641 1
|
存储 机器学习/深度学习 数据可视化
解析exe文件
如何使用`objdump`工具解析exe文件,包括exe文件的组成、`objdump`的用法以及如何查看exe文件的节头信息和完整内容。
846 0
解析exe文件
|
存储
串行口通信原理及操作流程
串行口通信是一种将数据以串行方式传输的通信方式,它通过一根传输线(串行线)将数据位逐位地传输,相比并行通信,串行通信可以减少传输线的数量,提高传输效率。以下是串行口通信的原理及操作流程的详细介绍。 1. 原理: 串行口通信使用串行通信协议进行数据传输。常见的串行通信协议包括RS-232、RS-485、UART等。这些协议规定了数据传输的格式、波特率、起始位、停止位、校验位等参数。 在串行口通信中,数据被分割成多个数据位,每个数据位逐个传输。数据位之间通过特定的时钟信号进行同步。发送端将数据位按照协议规定的格式发送到传输线上,接收端通过解析接收到的数据位来恢复原始数据。通过这种方式,数据可以
738 0
|
Ubuntu
简单几步实现Ubuntu22.04启用Nvidia显卡
本文是关于如何在Ubuntu 22.04操作系统上启用Nvidia显卡的教程,包括禁用旧驱动、添加新驱动源、安装推荐驱动、重启系统以及通过Nvidia设置更改为高性能模式的步骤。
5306 0
简单几步实现Ubuntu22.04启用Nvidia显卡
|
开发框架 前端开发 JavaScript
在DevExpress的GridView的列中,动态创建列的时候,绑定不同的编辑处理控件
在DevExpress的GridView的列中,动态创建列的时候,绑定不同的编辑处理控件
|
传感器 数据采集 物联网
探索未来:.NET nanoFramework引领嵌入式设备编程革新之旅
【8月更文挑战第28天】.NET nanoFramework 是一款专为资源受限的嵌入式设备设计的轻量级、高性能框架,基于 .NET Core,采用 C# 进行开发,简化了传统底层硬件操作的复杂性,极大提升了开发效率。开发者可通过 Visual Studio 或 Visual Studio Code 快速搭建环境并创建项目,利用丰富的库和驱动程序轻松实现从基础 LED 控制到网络通信等多种功能,显著降低了嵌入式开发的门槛。
401 0
|
存储 Kubernetes jenkins
在k8S中,Jenkins发布详细流程是什么?
在k8S中,Jenkins发布详细流程是什么?
|
Android开发
Android获取当前系统日期和时间的三种方法
Android获取当前系统日期和时间的三种方法
1295 4