为什么浮动这样做?

Kev*_*tta 0 c floating-point

我正在玩普通的旧C.我只是计算一些便士,然后在一定次数的迭代中以指数方式增加数量.在这个过程的最后,我想要在屏幕上以美元和美分显示大量的美分/便士.

我的输出电流如下所示:

"You have 805306365 pennies"
Run Code Online (Sandbox Code Playgroud)

我试图通过将一个新变量定义为float来将此数字转换为美元:

float totalD = total / 100.00;
Run Code Online (Sandbox Code Playgroud)

输出显示8053063.50.为什么这个int的划分造成了15美分的损失?我试图通过"printf"显示它,如果这有助于:

printf("You have %.2f dollars", totalD);
Run Code Online (Sandbox Code Playgroud)

我知道我可以把我的美分转换成一个字符串,并尝试格式化它,但我喜欢,但我很困惑为什么一个浮点数会这样做.任何人都可以告诉我为什么会发生这种情况,以及如何处理它?

Flo*_*ris 5

更新了准确性和完整性

您遇到的是浮点数中的(相当常见的)精度问题.在许多编译器中,浮点数只有32位长,并且它们使用一定数量的这些位(23)作为其"有效数"(有时称为"尾数"),1符号位和8位用于" exponent"(在1.23E45之类的数字中,"1.23"是有效数字,而"45"是指数.在二进制中,你实际上有相对较少的(24)个零和零可用,所以你的精确度在周围十进制表示法中的数字6或7).

为了说明这种精度的损失,我写了几行代码:

#include <stdio.h>

int main(){
  float fpennies;
  long lpennies, ii;

  lpennies = 805306360;
  for(ii = 0; ii< 100; ii++) {
      fpennies = lpennies + ii;
      printf("%ld pennies converted to float: %.0f fpennies\n",ii+ lpennies, fpennies);
  }
}
Run Code Online (Sandbox Code Playgroud)

这产生了许多类型的线

805306360 pennies converted to float: 805306368 fpennies 
805306361 pennies converted to float: 805306368 fpennies 
805306362 pennies converted to float: 805306368 fpennies 
... 
805306400 pennies converted to float: 805306368 fpennies 
805306401 pennies converted to float: 805306432 fpennies
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,对各地805306400,递增long的只是一个递增float的数字的表示64!通过稍微查看浮点数的二进制表示可以最好地解释这一点.

首先,这是一个32位浮点数的组织(来自http://upload.wikimedia.org/wikipedia/commons/d/d2/Float_example.svg):

在此输入图像描述

我们可以使用一些显式的转换来获取数字的十六进制表示:

printf("%.0f %08x", fpennies, *(unsigned int*)(&fpennies));
Run Code Online (Sandbox Code Playgroud)

对于我们之前看到的跨越跳跃的两个值,这会导致

805306368 4e400000
805306432 4e400001
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,有效数字的"最低有效位"增加了1,但指数意味着乘数为64.为什么64?好吧,让我们扩展前几位:

0x4e40 = 0100 1110 0100 0000 in binary
Run Code Online (Sandbox Code Playgroud)

由于最高位是符号位(0 =正),接下来的8位是指数,因此这是指数

1001 1100 = 0x9c = 156
Run Code Online (Sandbox Code Playgroud)

现在从浮点中的位到其值的规则(参见http://en.wikipedia.org/wiki/Single-precision_floating-point_format)是

value = (-1)^(sign bit) * (1 + sum(i=1 to 23, bit(23-i)*2^(-i))) * 2^(exponent - 127)
Run Code Online (Sandbox Code Playgroud)

在这种情况下,1最低有效位(位0)的变化增加2^(-23) * 2^( 156 - 127 ) = 2^6 = 64

因此,对于这个数量的数字,可以表示的最小步长64,如输出中所示.

如果你想解决这个问题,你可以做一些在Vaughn的答案中建议的事情 - 使用代表便士的长整数,并使用整数数学(除法,模数)来获得"全部美元,全部美分"金额.

long int dollars, cents, pennies;
...
dollars = pennies / 100;
cents = pennies % 100;
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以在不损失精确度的情况下代表一些相当大的金钱.

在实践中,当你写作

float pennies = 805306365;
printf("you have %f pennies\n", pennies);
Run Code Online (Sandbox Code Playgroud)

你得到

You have 805306368 pennies
Run Code Online (Sandbox Code Playgroud)

如果你使用一种double类型,你会有更好的运气(在这种情况下).

  • 我确定这是一个错字.通常,`float`使用8位作为幅度,使用24("24-1"物理位)作为有效数. (2认同)
  • 我想你会发现有1位用于符号,8位用于指数,23位显式位和1位隐含位用于尾数.请参阅[IEEE 754]上的维基百科(http://en.wikipedia.org/wiki/IEEE_floating_point). (2认同)