首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >为什么 PHP 里 0.1 + 0.2 不等于 0.3?正确计算方案全解

为什么 PHP 里 0.1 + 0.2 不等于 0.3?正确计算方案全解

作者头像
Tinywan
发布于 2026-09-15 15:12:18
发布于 2026-09-15 15:12:18
1430
举报
文章被收录于专栏:开源技术小栈开源技术小栈

如何在PHP中正确计算 0.1 + 0.2?

阅读前先来做个小测试:

var_dump(0.1 + 0.2 === 0.3);

这段代码会返回 false。作为熟悉IEEE浮点数规则的开发者,很多人可能会抿一口咖啡说:嗨,这早就不是什么新鲜事了。我们都知道这个经典案例,它提醒着一代又一代初级开发者:直接用浮点数计算可能会引发bug。

不过,我们来看下面这个例子(64位环境下):

var_dump((9223372036854775808 - 1) === 9223372036854775807);

很明显这是整数运算,肯定不会受那些奇怪的浮点数特性影响,结果应该是true对吧。我们总不至于在这里也要用高精度数学扩展(BCMath、GMP)吧? 对吧?

PHP浮点数的工作原理

PHP的float类型依赖于平台,但通常采用IEEE 754 binary64(双精度)格式,包含符号位、指数位和53位二进制精度。PHP手册中说明,其实际精度约为14位十进制数,最大相对舍入误差约为1.11e-16。

这里的关键词是二进制。浮点数表示的是2的幂次的有限和。像0.5(1/2)和0.125(1/8)这样的小数,其二进制表示是有限的,因此可以被精确存储。但0.1(1/10)做不到:它的二进制表示是无限循环的,就像十进制里1/3永远循环一样。

因此PHP只会存储最接近的可表示二进制值。我们可以通过打印足够多的小数位,来直观看到这些近似值:

printf("%.17g\n", 0.1);         // 输出 0.10000000000000001printf("%.17g\n", 0.2);         // 输出 0.20000000000000001printf("%.17g\n", 0.1 + 0.2);   // 输出 0.30000000000000004

精度丢失的场景

▪ 浮点数运算的精度问题

最初的比较中,等号两边都是浮点数,但它们对应了两个不同的邻近二进制值:

$sum = 0.1 + 0.2; // float(0.30000000000000004)var_dump($sum === 0.3);              // bool(false)var_dump(abs($sum - 0.3) < 1e-12);   // bool(true)

▪ 大整数运算的溢出问题

PHP的整数是有符号的,且依赖于平台。整数的最大值由PHP_INT_MAX定义。

在64位编译版本中,整数范围的上限通常是9223372036854775807。根据手册中的整数溢出规则,当字面量或运算结果超出整数范围时,就会转为浮点数:

var_dump(PHP_INT_MAX);       // int(9223372036854775807)var_dump(PHP_INT_MAX + 1);   // float(9.223372036854776E+18)

问题就在这里!binary64浮点数的数值范围足以容纳这么大的数,但精度不足以区分这个量级下的每一个整数。我们回到开头的例子:

var_dump(((PHP_INT_MAX + 1) - 1) === PHP_INT_MAX);           // bool(false)var_dump((9223372036854775808 - 1) === 9223372036854775807); // bool(false)(64位环境)

一旦整数溢出转为浮点数,再转换回整数也无法找回丢失的低位数字。

如何在PHP中正确进行浮点数计算

好吧,浮点数确实很反直觉,很多人可能会这么说。但PHP里有没有办法让这些计算得到正确结果?当然有!

▪ 安全使用原生浮点数

浮点数占用空间小、运算速度快,非常适合处理近似的数值。正确使用它们的前提,是接受最后几位数字并非精确值这一事实。以下是一些正确使用PHP原生浮点数的建议:

切勿盲目将浮点数强制转换为整数!

又到测试时间!下面这段代码会输出什么?

$value = 0.58 * 100; echo $value, "\n"; var_dump(intval($value));var_dump((int) $value);

输出结果是:

58int(57)int(57)

是不是很有意思?intval()和(int)强制转换都不会四舍五入到最近的整数,而是直接舍去小数部分,向零取整。如果这个计算需要得到最接近的整数值,先调用round()再转换就能得到预期结果:

$value = 0.58 * 100;$result = (int) round($value); var_dump($result); // int(58)

所以将浮点数转为整数时一定要谨慎。

有人可能会问:等等,那为什么echo $value, "\n";会输出58?这就引出了第二个问题:

INI配置项precision无法修正计算结果

ini_set('precision', '14'); // 14是默认值$value = 0.58 * 100; echo $value, "\n";        // 输出 58var_dump(intval($value)); // int(57)

为什么同一个值,用echo打印显示是58,转换为整数却变成了57?因为echo打印浮点数时,会使用配置的precision参数,这个参数会“隐藏”末尾的数位。而整数转换显然不会用这个四舍五入后的字符串,它直接读取内存中存储的浮点数本身——而存储的值其实略小于58。

php.ini中的precision指令,只控制浮点数转换为字符串时显示的位数,它不会改变内存中的数值,也不会改变处理器执行的算术运算:

$sum = 0.1 + 0.2; ini_set('precision', '17');echo $sum, "\n"; // 输出 0.30000000000000004 ini_set('precision', '14');echo $sum, "\n"; // 输出 0.3 var_dump($sum === 0.3); // bool(false)

两次echo读取的是同一个浮点数。一种表示隐藏了末尾数位,另一种显示了全部,但两者都不会改变严格比较的结果。

PHP还有一个独立的配置项serialize_precision。顾名思义,它不仅控制serialize()输出的浮点数文本表示,还会影响json_encode()、var_dump()等函数的输出。但它同样不会影响产生这些数值的算术运算。它们都不是数学运算层面的功能。

基于容差比较浮点数是否相等

当数值允许近似时,PHP手册建议使用可接受的误差范围来比较浮点数,而不是直接判断相等。前面用到的简单校验方式,在所有数值量级相近且已知的场景下是有效的:

$actual = 0.1 + 0.2;$expected = 0.3; var_dump(abs($actual - $expected) < 1e-12); // bool(true)

容差值需要根据业务场景来定。1e-12适合这个小示例,但它绝不是适用于所有计算的万能值。

PHP_FLOAT_EPSILON描述的是1.0附近可表示浮点数的最小间隔;它并不是自动适用于所有数量级、所有业务场景的正确容差。更多关于epsilon方法的基础说明,可以参考手册中浮点数比较的相关指引。

用整数实现精确的固定小数位运算

如果数值有固定的最小单位,以这个最小单位为基准用整数存储,可以完全避免二进制小数的问题。例如,金额通常以分为单位存储:

$unitPriceInCents = 58; // 单价(分)$quantity = 3;          // 数量 $totalInCents = $unitPriceInCents * $quantity; var_dump($totalInCents); // int(174)

转换为最小单位的过程也必须是精确的。不要先接收0.58这样的浮点数,再假设(int) ($value * 100)是安全的——这和我们上面看到的转换bug是同一个问题。应该在输入边界处解析经过校验的十进制字符串,或者使用十进制运算。

原生整数仍然有PHP_INT_MAX的上限。如果换算后的值可能超出这个范围,从一开始就应该使用任意精度的表示方式。

▪ 核心扩展可以帮到你!

害,我可不想记住这些规则,也不想为了精度把所有浮点数都转成整数、改单位。没关系,核心扩展这就来帮你。

处理浮点数?使用BCMath

BCMath执行任意精度的十进制算术运算。它的传统函数接收和返回的都是字符串,因此十进制输入不会先经过二进制浮点数转换:

$sum = bcadd('0.1', '0.2', 1);$scaled = bcmul('0.58', '100', 0); var_dump($sum);    // string(3) "0.3"var_dump($scaled); // string(2) "58"

PHP 8.4及更高版本还提供了不可变的BcMath\Number对象,支持普通的算术运算符:

use BcMath\Number; $sum = new Number('0.1') + new Number('0.2'); echo $sum, "\n"; // 输出 0.3

引号很重要。要从'0.1'这样的十进制字符串构建运算,而不是用可能已经丢失精度的浮点数。事后把一个近似的浮点数转为BCMath值,是无法还原最初精确的十进制输入的。

BCMath非常适合价格、余额、利率等小数位有明确含义的数值场景。

处理大整数?使用GMP

GMP扩展用于处理任意长度的整数。它默认不开启,需要依赖外部GMP库。它可以无溢出地计算开头的大整数例子,不会转为浮点数:

$number = gmp_init('9223372036854775808', 10);$result = gmp_sub($number, '1'); echo gmp_strval($result), "\n"; // 输出 9223372036854775807

同样,大的输入值必须用字符串形式。如果直接写9223372036854775808这个PHP数字字面量,PHP会先把它转为浮点数,再传给GMP——这时精度已经丢失了。

GMP表示的是整数而非十进制小数,因此不能直接存储0.1。如果业务中用最小单位的整数来表示所有数值,它也可以处理固定小数位的场景。但当小数位数本身就是数据的一部分时,BCMath通常更清晰易用。

结论

▪ 如何在PHP中正确计算0.1加0.2?

那么,PHP应该怎样计算0.1 + 0.2呢?如果数值本身就是近似值,原生浮点数加法已经在执行预期的二进制运算了。用合适的容差比较结果,再格式化输出即可。如果必须得到精确的十进制结果0.3,那就要从十进制表示开始计算:

echo bcadd('0.1', '0.2', 1); // 输出 0.3

没有任何php.ini开关能把二进制浮点数变成精确的十进制数。正确性来源于计算开始前就选对数值的表示方式。

持续演进中

BCMath和GMP都在持续发展。例如,PHP 8.6为GMP新增了两个函数:gmp_prev_prime和gmp_powm_sec,都是非常实用的功能。PHP语言正在快速迭代,让每一位PHP开发者的开发工作更轻松。

PHP语言生生不息,承载着所有贡献者的热情、付出与纯粹的热爱。感谢你阅读本文,感谢你使用PHP!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-09-14,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 如何在PHP中正确计算 0.1 + 0.2?
    • PHP浮点数的工作原理
    • 精度丢失的场景
      • ▪ 浮点数运算的精度问题
      • ▪ 大整数运算的溢出问题
    • 如何在PHP中正确进行浮点数计算
      • ▪ 安全使用原生浮点数
      • ▪ 核心扩展可以帮到你!
    • 结论
      • ▪ 如何在PHP中正确计算0.1加0.2?
    • 持续演进中
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档