考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些错误?
考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些错误?
当前回答
十进制数(如0.1、0.2和0.3)在二进制编码浮点类型中没有精确表示。0.1和0.2的近似值之和与0.3的近似值不同,因此,0.1+0.2==0.3的错误在这里可以更清楚地看到:
#include <stdio.h>
int main() {
printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
printf("0.1 is %.23f\n", 0.1);
printf("0.2 is %.23f\n", 0.2);
printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
printf("0.3 is %.23f\n", 0.3);
printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
return 0;
}
输出:
0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17
为了更可靠地计算这些计算,您需要对浮点值使用基于十进制的表示。C标准没有默认指定此类类型,而是作为技术报告中描述的扩展。
_Decimal32、_Decimal64和_Decimal128类型可能在您的系统上可用(例如,GCC在选定的目标上支持它们,但Clang在OS X上不支持它们)。
其他回答
简而言之,这是因为:
浮点数不能以二进制精确表示所有小数
因此,就像10/3不精确地存在于基数10中(它将是3.33……重复出现)一样,1/10也不存在于二进制中。
那又怎么样?如何处理?有什么解决办法吗?
为了提供最佳解决方案,我可以说我发现了以下方法:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
让我解释一下为什么这是最好的解决方案。正如上面提到的其他答案一样,使用现成的Javascript toFixed()函数来解决问题是一个好主意。但很可能你会遇到一些问题。
假设你将两个浮点数相加,如0.2和0.7,这里是:0.2+0.7=0.8999999999999999。
您的预期结果是0.9,这意味着您需要一个精度为1位数的结果。因此,您应该使用(0.2+0.7).tfixed(1)但是不能只给toFixed()一个特定的参数,因为它取决于给定的数字,例如
0.22 + 0.7 = 0.9199999999999999
在本例中,您需要2位精度,因此它应该为Fixed(2),那么,适合每个给定浮点数的参数应该是什么?
你可以说在每种情况下都是10:
(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"
该死你打算怎么处理那些9后不需要的零?现在是将其转换为浮动的时候了,以实现您的愿望:
parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9
既然找到了解决方案,那么最好将其作为如下函数提供:
function floatify(number){
return parseFloat((number).toFixed(10));
}
让我们自己试试吧:函数floatify(数字){return parseFloat((number).toFixed(10));}函数addUp(){var number1=+$(“#number1”).val();var number2=+$(“#number2”).val();var expectedResult=number1+number2;var expectedResult=浮动(number1+number2);$(“#意外结果”).text(意外结果);$(“#expectedResult”).text(expectedResult);}addUp();输入{宽度:50px;}#预期结果{颜色:绿色;}#未预期结果{颜色:红色;}<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><input id=“number1”value=“0.2”onclick=“addUp()”onkeyup=“addUp()”/>+<input id=“number2”value=“0.7”onclick=“addUp()”onkeyup=“addUp()”/>=<p>预期结果:<span id=“expectedResult”></span></p><p>意外结果:<span id=“expectedResult”></span></p>
您可以这样使用:
var x = 0.2 + 0.7;
floatify(x); => Result: 0.9
正如W3SCHOOLS所建议的,还有另一种解决方案,您可以通过乘法和除法来解决上述问题:
var x = (0.2 * 10 + 0.1 * 10) / 10; // x will be 0.3
请记住,(0.2+0.1)*10/10根本不起作用,尽管看起来是一样的!我更喜欢第一种解决方案,因为我可以将其作为一个函数应用,将输入浮点转换为精确的输出浮点。
仅供参考,乘法也存在同样的问题,例如0.09*10返回0.8999999999999999。应用flotify函数作为解决方法:flotify(0.09*10)返回0.9
它被打破的方式与你在小学学习并每天使用的十进制(以10为基础)表示法完全相同,只是以2为基础。
要理解,请考虑将1/3表示为十进制值。这是不可能做到的!世界将在你写完小数点后的3之前结束,所以我们写了一些地方,认为它足够准确。
以同样的方式,1/10(十进制0.1)不能以2为基数(二进制)精确地表示为“十进制”值;小数点后的重复模式将永远持续下去。该值不精确,因此无法使用常规浮点方法对其进行精确计算。与基数10一样,还有其他值也显示了这个问题。
我的答案很长,所以我把它分成了三部分。因为这个问题是关于浮点数学的,所以我把重点放在了机器的实际功能上。我还将其指定为双精度(64位),但该参数同样适用于任何浮点运算。
序言
IEEE 754双精度二进制浮点格式(binary64)数字表示以下形式的数字
值=(-1)^s*(1.m51m50…m2m1m0)2*2e-1023
64位:
第一位是符号位:如果数字为负,则为1,否则为0。接下来的11位是指数,偏移1023。换句话说,在从双精度数字中读取指数位之后,必须减去1023以获得2的幂。剩下的52位是有效位(或尾数)。在尾数中,“隐含”1。由于任何二进制值的最高有效位为1,因此总是省略2。
1-IEEE 754允许有符号零的概念-+0和-0被不同地对待:1/(+0)是正无穷大;1/(-0)是负无穷大。对于零值,尾数和指数位均为零。注意:零值(+0和-0)未明确归为非标准2。
2-非正规数的情况并非如此,其偏移指数为零(以及隐含的0)。非正规双精度数的范围为dmin≤|x|≤dmax,其中dmin(最小的可表示非零数)为2-1023-51(≈4.94*10-324),dmax(最大的非正规数,其尾数完全由1组成)为2-1023+1-21-23-51(≈2.225*10-308)。
将双精度数字转换为二进制
存在许多在线转换器来将双精度浮点数转换为二进制(例如,在binaryconvert.com),但这里有一些示例C#代码来获得双精度数字的IEEE 754表示(我用冒号(:)分隔这三个部分:
public static string BinaryRepresentation(double value)
{
long valueInLongType = BitConverter.DoubleToInt64Bits(value);
string bits = Convert.ToString(valueInLongType, 2);
string leadingZeros = new string('0', 64 - bits.Length);
string binaryRepresentation = leadingZeros + bits;
string sign = binaryRepresentation[0].ToString();
string exponent = binaryRepresentation.Substring(1, 11);
string mantissa = binaryRepresentation.Substring(12);
return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}
开门见山:最初的问题
(对于TL;DR版本,跳到底部)
卡托·约翰斯顿(提问者)问为什么0.1+0.2!=0.3.
以二进制(用冒号分隔三个部分)编写,IEEE 754值表示为:
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010
请注意,尾数由0011的重复数字组成。这是为什么计算有任何错误的关键-0.1、0.2和0.3不能用二进制精确地表示在有限数量的二进制位中,任何超过1/9、1/3或1/7的二进制位都可以用十进制数字精确地表示。
还要注意,我们可以将指数的幂减小52,并将二进制表示中的点向右移动52位(非常类似10-3*1.23==10-5*123)。这使我们能够将二进制表示表示为它以a*2p形式表示的精确值。其中“a”是整数。
将指数转换为十进制、删除偏移量并重新添加隐含的1(在方括号中)、0.1和0.2为:
0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125
要添加两个数字,指数必须相同,即:
0.1 => 2^-3 * 0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 * 1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125
sum = 2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875
由于和的形式不是2n*1.{bbb},我们将指数增加1,并移动小数(二进制)点以获得:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
现在尾数中有53位(第53位在上一行的方括号中)。IEEE 754的默认舍入模式是“舍入到最近”,即如果数字x介于两个值a和b之间,则选择最低有效位为零的值。
a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
= 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
注意,a和b仅在最后一位不同。。。0011 + 1 = ...0100。在这种情况下,最低有效位为零的值为b,因此总和为:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
而0.3的二进制表示是:
0.3 => 2^-2 * 1.0011001100110011001100110011001100110011001100110011
= 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
其仅与0.1和0.2之和的二进制表示相差2-54。
0.1和0.2的二进制表示是IEEE 754允许的数字的最精确表示。由于默认舍入模式,添加这些表示会导致一个仅在最低有效位不同的值。
TL;博士
将0.1+0.2写入IEEE 754二进制表示(用冒号分隔三个部分),并将其与0.3进行比较,这是(我将不同的位放在方括号中):
0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3 => 0:01111111101:0011001100110011001100110011001100110011001100110[011]
转换回十进制,这些值为:
0.1 + 0.2 => 0.300000000000000044408920985006...
0.3 => 0.299999999999999988897769753748...
与原始值相比,差异正好为2-54,约为5.5511151231258×10-17(对于许多应用)。
比较浮点数的最后几位本来就很危险,任何读过著名的《每一位计算机科学家都应该知道的关于浮点运算》(该书涵盖了这个答案的所有主要部分)的人都会知道。
大多数计算器使用额外的保护数字来解决这个问题,这就是0.1+0.2如何给出0.3:最后几位是四舍五入的。
正常的算术是以10为基数的,所以小数表示十分、百分等。当你试图用二进制2为基数的算术表示浮点数时,你要处理的是半、四、八等。
在硬件中,浮点存储为整数尾数和指数。尾数表示有效数字。指数类似于科学记数法,但它使用的基数是2而不是10。例如,64.0将用尾数1和指数6表示。0.125将用尾数1和指数-3表示。
浮点小数必须加上2的负幂
0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d
等等
在处理浮点运算时,通常使用误差增量而不是相等运算符。而不是
if(a==b) ...
你会使用
delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...
存储在计算机中的浮点数由两部分组成,一部分是整数,另一部分是基数乘以整数部分的指数。
如果计算机在基数为10的情况下工作,则0.1将是1 x 10⁻¹,0.2将是2 x 10⁻¹,0.3将是3 x 10⁻¹. 整数运算简单而准确,所以加上0.1+0.2显然会得到0.3。
计算机通常不以10为基数工作,而是以2为基数工作。对于某些值,仍然可以得到精确的结果,例如0.5是1 x 2⁻¹和0.25是1 x 2⁻²,将它们相加,结果为3 x 2⁻²或0.75。确切地
问题是数字可以精确地以10为基数表示,但不能以2为基数。这些数字需要四舍五入到最接近的相等值。假设非常常见的IEEE 64位浮点格式,最接近0.1的数字是3602879701896397 x 2⁻⁵⁵, 最接近0.2的数字是7205759403792794 x 2⁻⁵⁵; 将它们相加,得到10808639105689191 x 2⁻⁵⁵, 或精确的十进制值0.30000000000000000444089209850062616169452667236328125。浮点数通常四舍五入以显示。