92
$a = '35';
$b = '-34.99';
echo ($a + $b);

结果为 0.009999999999998

那是怎么回事?我想知道为什么我的程序一直报告奇怪的结果。

为什么 PHP 不返回预期的 0.01?

4

8 回答 8

132

因为浮点运算!=实数运算。由于不精确造成的差异的一个例子是,对于一些浮点数ab, (a+b)-b != a。这适用于任何使用浮点数的语言。

由于浮点数是具有有限精度的二进制数,因此可表示数字的数量是有限的,这会导致精度问题和类似这样的意外。这是另一篇有趣的读物:每个计算机科学家都应该知道的关于浮点运算的知识


回到你的问题,基本上没有办法用二进制准确表示 34.99 或 0.01(就像十进制一样,1/3 = 0.3333 ...),所以使用近似值。要解决此问题,您可以:

  1. 使用round($result, 2)结果将其四舍五入到小数点后 2 位。

  2. 使用整数。如果那是货币,比如美元,则将 35.00 美元存储为 3500,将 34.99 美元存储为 3499,然后将结果除以 100。

遗憾的是 PHP 没有像其他 语言那样具有十进制数据类型。

于 2010-09-16T12:43:21.450 回答
61

与所有数字一样,浮点数必须作为 0 和 1 的字符串存储在内存中。对电脑来说都是零碎的。浮点数与整数的不同之处在于我们在查看 0 和 1 时如何解释它们。

一位是“符号”(0 = 正,1 = 负),8 位是指数(范围从 -128 到 +127),23 位是称为“尾数”(分数)的数字。所以 (S1)(P8)(M23) 的二进制表示具有值 (-1^S)M*2^P

“尾数”采用特殊形式。在正常的科学记数法中,我们将“一个人的位置”与分数一起显示。例如:

4.39 x 10^2 = 439

在二进制中,“一个位置”是一个位。由于我们忽略了科学记数法中所有最左边的 0(我们忽略了任何无关紧要的数字),因此第一位保证为 1

1.101 x 2^3 = 1101 = 13

由于我们保证第一位将为 1,因此我们在存储数字时删除该位以节省空间。所以上面的数字只存储为 101(尾数)。假设前导 1

举个例子,让我们以二进制字符串为例

00000010010110000000000000000000

把它分解成它的组件:

Sign    Power           Mantissa
 0     00000100   10110000000000000000000
 +        +4             1.1011
 +        +4       1 + .5 + .125 + .0625
 +        +4             1.6875

应用我们的简单公式:

(-1^S)M*2^P
(-1^0)(1.6875)*2^(+4)
(1)(1.6875)*(16)
27

换句话说,00000010010110000000000000000000 是 27 的浮点数(根据 IEEE-754 标准)。

然而,对于许多数字来说,并没有精确的二进制表示。就像 1/3 = 0.333.... 永远重复一样,1/100 是 0.00000010100011110101110000 ... 重复“10100011110101110000”。但是,32 位计算机无法将整个数字存储为浮点数。所以它做出了最好的猜测。

0.0000001010001111010111000010100011110101110000

Sign    Power           Mantissa
 +        -7     1.01000111101011100001010
 0    -00000111   01000111101011100001010
 0     11111001   01000111101011100001010
01111100101000111101011100001010

(注意负 7 是使用 2 的补码产生的)

应该立即清楚 01111100101000111101011100001010 看起来不像 0.01

然而,更重要的是,它包含重复小数的截断版本。原始小数包含重复的“10100011110101110000”。我们已将其简化为 01000111101011100001010

通过我们的公式将此浮点数转换回十进制,我们得到 0.0099999979(请注意,这是针对 32 位计算机的。64 位计算机将具有更高的精度)

十进制等价物

如果它有助于更​​好地理解问题,让我们在处理重复小数时看一下十进制科学记数法。

假设我们有 10 个“盒子”来存储数字。因此,如果我们想存储一个像 1/16 这样的数字,我们会这样写:

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
+---+---+---+---+---+---+---+---+---+---+

这明明只是6.25 e -2,哪里e是简写*10^(。尽管我们只需要 2 个(用零填充),但我们为小数分配了 4 个框,并且我们为符号分配了 2 个框(一个用于数字的符号,一个用于指数的符号)

使用这样的 10 个框,我们可以显示从-9.9999 e -9+9.9999 e +9

这适用于小数位数为 4 或更少的任何东西,但是当我们尝试存储类似的数字时会发生什么2/3

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

这个新数字0.66667并不完全相等2/3。事实上,它已经关闭了0.000003333...。如果我们尝试0.66667以 3 为底编写,我们会得到0.2000000000012...而不是0.2

如果我们采用更大的重复小数,例如1/7. 这有 6 个重复数字:0.142857142857...

将其存储到我们的十进制计算机中,我们只能显示其中的 5 个数字:

+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

这个数字 ,0.14286关闭.000002857...

它“接近正确”,但并不完全正确,因此如果我们尝试以 7 为底写这个数字,我们会得到一些可怕的数字而不是0.1. 事实上,将其插入 Wolfram Alpha 中,我们得到:.10000022320335...

这些微小的分数差异对您来说应该很熟悉0.0099999979(而不是0.01

于 2010-09-29T02:59:01.823 回答
17

这里有很多关于为什么浮点数以它们的方式工作的答案......

但是很少有人谈论任意精度(Pickle 提到了它)。如果您想要(或需要)精确的精度,唯一的方法(至少对于有理数)是使用BC 数学扩展(这实际上只是一个BigNum,任意精度的实现......

添加两个数字:

$number = '12345678901234.1234567890';
$number2 = '1';
echo bcadd($number, $number2);

将导致12345678901235.1234567890...

这称为任意精度数学。基本上所有数字都是为每个操作解析的字符串,并且操作是逐位执行的(考虑长除法,但由库完成)。所以这意味着它非常慢(与常规数学结构相比)。但它非常强大。您可以对任何具有精确字符串表示的数字进行乘、加、减、除、求模和取幂。

所以你不能做到1/3100% 的准确度,因为它有一个重复的小数(因此不合理)。

但是,如果你想知道什么是1500.0015平方:

使用 32 位浮点数(双精度)给出以下估计结果:

2250004.5000023

但是 bcmath 给出了准确的答案:

2250004.50000225

这完全取决于您需要的精度。

另外,这里还有一点需要注意。PHP 只能表示 32 位或 64 位整数(取决于您的安装)。因此,如果一个整数超过了本机 int 类型的大小(32 位为 21 亿,9.2 x10^18 或有符号整数为 92 亿),PHP 会将 int 转换为浮点数。虽然这不是一个直接的问题(因为所有小于系统浮点精度的整数根据定义都可以直接表示为浮点数),但如果您尝试将两个相乘,它将失去显着的精度。

例如,给定$n = '40000000002'

作为一个数字,$nwill be float(40000000002),这很好,因为它被精确地表示了。但是如果我们把它平方,我们得到:float(1.60000000016E+21)

作为一个字符串(使用 BC 数学),$n将是'40000000002'. 如果我们平方它,我们得到:string(22) "1600000000160000000004"...

因此,如果您需要大数或有理小数点的精度,您可能需要查看 bcmath ...

于 2010-10-01T18:33:54.547 回答
4

bcadd()在这里可能有用。

<?PHP

$a = '35';
$b = '-34.99';

echo $a + $b;
echo '<br />';
echo bcadd($a,$b,2);

?>

(为清晰起见,输出效率低下)

第一行给了我 0.009999999999998。第二个给我 0.01

于 2010-09-29T19:49:15.507 回答
3

因为 0.01 不能完全表示为二进制分数系列的总和。这就是浮点数存储在内存中的方式。

我想这不是你想听到的,但它是对问题的回答。有关如何修复,请参阅其他答案。

于 2010-09-16T12:42:58.750 回答
1

使用 PHP 的round()函数: http: //php.net/manual/en/function.round.php

这个答案解决了问题,但没有解释原因。我认为这很明显[我也在用 C++ 编程,所以这对我来说很明显;]],但如果不是,假设 PHP 有它自己的计算精度,并且在那种特殊情况下,它返回了关于该计算的最符合的信息.

于 2010-09-16T12:40:53.217 回答
1

每个数字都会以 0、1 等二进制值保存在计算机中。在单精度数字中,占 32 位。

浮点数可以表示为:1 位表示符号,8 位表示指数,23 位称为尾数(分数)。

看下面的例子:

0.15625 = 0.00101 = 1.01*2^(-3)

在此处输入图像描述

  • 符号:0表示正数,1表示负数,本例为0。

  • 指数:01111100 = 127 - 3 = 124。

    注意:偏差 = 127 所以偏差指数 = -3 + “偏差”。在单精度下,偏差为 ,127,因此在此示例中,偏差指数为 124;

  • 在小数部分,我们有:1.01 意思是:0*2^-1 + 1*2^-2

    数字 1(1.01 的第一个位置)不需要保存,因为以这种方式呈现浮点数时,第一个数字始终为 1。例如转换:0.11 => 1.1*2^(-1), 0.01 => 1* 2^(-2)。

另一个示例显示始终删除第一个零:0.1 将显示为 1*2^(-1)。所以第一个总是 1。当前 1*2^(-1) 的数量将是:

  • 0:正数
  • 127-1 = 126 = 01111110
  • 分数:00000000000000000000000(23个数字)

最后:原始二进制文件是:0 01111110 00000000000000000000000

在这里检查:http: //www.binaryconvert.com/result_float.html?decimal=048046053

现在,如果您已经了解如何保存浮点数。如果数字无法保存为 32 位(简单精度)会发生什么情况。

例如:十进制。1/3 = 0.3333333333333333333333 因为它是无限的,我想我们有 5 位来保存数据。再重复一遍,这不是真的。只是假设。因此保存在计算机中的数据将是:

0.33333.

现在,当加载计算机的数字再次计算时:

0.33333 = 3*10^-1 + 3*10^-2 + 3*10^-3 + 3*10^-4 +  3*10^-5.

对这个:

$a = '35';
$b = '-34.99';
echo ($a + $b);

结果为 0.01(十进制)。现在让我们用二进制显示这个数字。

0.01 (decimal) = 0 10001111 01011100001010001111 (01011100001010001111)*(binary)

在这里查看:http: //www.binaryconvert.com/result_double.html?decimal=048046048049

因为 (01011100001010001111) 就像 1/3 一样重复。所以计算机无法将这个数字保存在他们的内存中。它必须牺牲。这导致计算机不准确。

高级 (你必须有数学知识)那么为什么我们可以很容易地用十进制显示 0.01 而不能用二进制显示。

假设 0.01(十进制)的二进制分数是有限的。

So 0.01 = 2^x + 2^y... 2^-z
0.01 * (2^(x+y+...z)) =  (2^x + 2^y... 2^z)*(2^(x+y+...z)). This expression is true when (2^(x+y+...z)) = 100*x1. There are not integer n = x+y+...+z exists. 

=> So 0.01 (decimal) must be infine in binary.
于 2016-07-30T10:28:55.657 回答
0

不是更容易使用吗number_format(0.009999999999998, 2)$res = $a+$b; -> number_format($res, 2);

于 2013-09-04T15:10:19.853 回答