PHP浮点数用floor()截小数会出错,因二进制无法精确表示0.01等十进制小数,导致1.235×100实际为123.499...,floor后变123,结果为1.23而非1.24;根本解法是绕过浮点运算,用字符串或bcmath处理。

PHP浮点数保存小数时为什么用 floor() 会出错
直接对浮点数调用 floor(1.2345 * 100) / 100 看似能截断到两位小数,但 PHP 的浮点数底层是二进制表示,0.1、0.01 这类十进制小数无法精确存储。比如 1.235 * 100 实际可能是 123.49999999999999,floor() 后变成 123,再除以 100 得到 1.23——比预期少 0.005。
- 这不是
floor()的 bug,而是 IEEE 754 浮点精度限制 - 所有语言都存在,但 PHP 默认显示会四舍五入掩盖问题,一存库或比较就暴露
- 用
round($val, 2, PHP_ROUND_HALF_DOWN)也不可靠:它仍基于浮点运算,中间步骤已有误差
真正安全的「保留 N 位小数并向下取整」做法
必须绕过浮点运算,改用字符串或整数处理。核心思路:先放大成整数(无精度损失),再截断,最后转回小数字符串或 float(仅用于显示/传输,不参与计算)。
- 对金额、精度敏感场景,推荐始终用字符串存储,如
"1.23",数据库字段设为DECIMAL(10,2) - 若必须返回
float,用bcdiv()+bcmul()(bcmath 扩展需启用):$val = "1.2345";
$scaled = bcmul($val, "100", 0); // 得到 "123"
$result = bcdiv($scaled, "100", 2); // 得到 "1.23" - 没 bcmath?用
number_format()配合substr()截断(仅限正数):$val = 1.2345;
$s = number_format($val, 10, '.', ''); // 转成 "1.2345000000"
$dotPos = strpos($s, '.');
$truncated = substr($s, 0, $dotPos + 3); // 保留两位小数
$result = (float)$truncated;
round() 和 floor() 在小数截断中的适用边界
round() 适合「四舍五入到 N 位」,floor() 适合「向下取整到整数」,两者都不适合「保留 N 位小数且严格向下截断」这个需求。误用会导致:
-
round(1.235, 2)→ 可能得1.23或1.24(取决于 PHP 版本和浮点误差累积) -
floor(1.235 * 100) / 100→ 大概率得1.23,但1.2399999999999998 * 100会变成123,结果仍是1.23,丢失了本该保留的1.239中的9 - 唯一稳妥的向下截断是:先转字符串,找到小数点位置,硬截掉多余位数,不经过任何乘除浮点运算
数据库写入前必须做的校验
即使 PHP 层做了字符串截断,写入 MySQL 时如果字段是 FLOAT 或 DOUBLE,依然会引入新误差。务必:
立即学习“PHP免费学习笔记(深入)”;
- 字段类型用
DECIMAL(M,N),例如DECIMAL(10,2) - 插入值用字符串(PDO 绑定参数时传
'1.23'而非1.23),避免 PDO 自动转成浮点 - 查询回来后,用
string接收而非float,防止 PDO 再次解析失真 - 如果已用
FLOAT字段,只能靠应用层补偿:读出后立刻用bcadd($val, '0', 2)强制格式化
小数精度问题从来不是单点修复,从输入、计算、存储到输出,每一步的类型选择都会叠加影响。最省事的方案,是别让小数在浮点里多待一毫秒。











