理解从整数到浮点数的转换

Understanding casts from integer to float

有人可以解释一下 32 位机器上这个看起来很奇怪的输出吗?

#include <stdio.h>

int main() {
  printf("16777217 as float is %.1f\n",(float)16777217);
  printf("16777219 as float is %.1f\n",(float)16777219);

  return 0;
}

输出

16777217 as float is 16777216.0
16777219 as float is 16777220.0

奇怪的是 16777217 转换为较低的值而 16777219 转换为较高的值...

类型float没有那么重要。有效数只能容纳 24 位。其中 23 个已存储,第 24 个是 1 而未存储,因为有效数字已标准化。

read this 上面写着 "Integers in [ − 16777216 , 16777216 ] can be exactly represented",但你的不在那个范围内。

在IEEE-754基本32位二进制浮点格式中,从-16,777,216到+16,777,216的所有整数都是可表示的。从 16,777,216 到 33,554,432,只能表示偶数。然后,从 33,554,432 到 67,108,864,只能表示四的倍数。 (由于这个问题不需要讨论哪些数字是可表示的,所以我将省略解释并认为这是理所当然的。)

最常见的默认舍入模式是将精确的数学结果舍入到最接近的可表示值,如果出现平局,则舍入到其尾数低位为零的可表示值。

16,777,217 在两个可表示值 16,777,216 和 16,777,218 之间是等距的。这些值表示为 1000000000000000000000002•21 和 1000000000000000000000012•2 1。前者的尾数低位为0,所以选择它作为结果。

16,777,219 在两个可表示值 16,777,218 和 16,777,220 之间等距。这些值表示为 1000000000000000000000012•21 和 1000000000000000000000102•2 1。后者的尾数低位为0,所以选择它作为结果。

您可能听说过 "precision" 的概念,如 "this fractional representation has 3 digits of precision"。

这在定点表示中很容易考虑。比方说,如果我有小数点后三位精度,那么我可以精确地表示 1/2 = 0.5,我可以精确地表示 1/4 = 0.25,我可以精确地表示 1/8 = 0.125,但是如果我尝试表示 1/16,我可以 而不是 得到 0.0625;我将不得不接受 0.062 或 0.063。

但那是针对定点的。您使用的计算机使用 浮点数 ,这很像科学记数法。您得到一定数量的有效数字 总计 ,而不仅仅是小数点右边的数字。例如,如果浮点格式的精度为 3 位小数,则可以表示 0.123 但不能表示 0.1234,并且可以表示 0.0123 和 0.00123,但不能表示 0.01234 或 0.001234。如果小数点左边有数字,那么小数点右边的数字就会消失。您可以使用 1.23 但不能使用 1.234,使用 12.3 但不能使用 12.34,使用 123.0 但不能使用 123.4 或 123.anythingelse.

并且 - 您现在可能已经看到了这种模式 - 如果您使用的是只有三个有效数字的浮点格式,则您根本无法完全准确地表示所有大于 999 的数字,即使尽管他们没有小数部分。可以代表1230不能代表1234,代表12300不能代表12340

这就是十进制浮点格式。另一方面,您的计算机使用 binary 浮点格式,这最终会变得有些棘手。我们没有精确的小数位数,无法精确表示的数字即使是 10 或 100 的倍数也不会很好。

特别是,大多数机器上的类型 float 具有 24 个二进制位的精度,相当于 6-7 个十进制数字的精度。对于 16777217 这样的数字,这显然不够。

那么16777216和16777220这两个数字是从哪里来的呢?正如 Eric Postpischil 已经解释的那样,它最终是因为它们是 2 的倍数。如果我们查看附近数字的二进制表示,模式就会变得清晰:

16777208     111111111111111111111000
16777209     111111111111111111111001
16777210     111111111111111111111010
16777211     111111111111111111111011
16777212     111111111111111111111100
16777213     111111111111111111111101
16777214     111111111111111111111110
16777215     111111111111111111111111
16777216    1000000000000000000000000
16777218    1000000000000000000000010
16777220    1000000000000000000000100

16777215 是可以用 24 位精确表示的最大数字。之后就只能表示偶数了,因为低位是第25位,本质上必须是0.

浮动表示遵循一种类似于我们在日常生活中使用的方法,我们称之为指数表示。这是一个使用我们认为足以实际表示该值的数字的数字,我们称之为尾数或有效值,我们将乘以一个底数或基数,该值提升为我们称为指数的幂。简单来说:

num*base^exp

我们一般以10为底数,因为我们手上有10根手指,所以我们习惯了1e2这样的数字,也就是100=1*10^2

当然,我们很遗憾对这么小的数字使用指数表示法,但是 我们更愿意在处理非常大的数字时使用它,或者更好的是,当我们的数字有很多数字时,我们考虑足以代表我们正在评估的实体

正确的位数可以是我们可以用头脑处理多少,或者工程应用需要多少。当我们决定需要多少位数时,我们将不再关心我们将要处理的数字表示如何符合真实值。 IE。对于像 123456.789e5 这样的数字,可以理解的是,将 99 单位相加我们可以容忍四舍五入的表示形式并认为它无论如何都可以接受,否则我们应该更改表示形式并使用具有适当位数的不同表示形式如 12345678900.

在计算机上,当你必须处理非常大的数字时,不能用标准整数表示,或者当你必须表示实数(带小数部分)时,正确的选择是 floatingdouble 浮点表示。它使用 与我们上面讨论的布局相同,但基数是 2 而不是 10。这是因为计算机只能有 2 个手指,状态 01。把我们之前用的公式,表示100,变成:

100100*2^0

这仍然不是真正的浮点表示法,但给出了思路。现在考虑在计算机中浮点格式是标准化的,对于标准浮点数,根据 IEE-754,它使用内存布局(我们将在后面看到为什么假设尾数多 1 位),23 位用于尾数,1 位用于符号,8 位用于偏移 -127 的指数(这仅仅意味着它的范围在 -126+127 之间,不需要符号位,值 0x000xff 保留用于特殊含义)。

现在考虑使用 0 作为指数,这意味着值 2^exponent=2^0=1 乘以尾数给出与 23 位整数相同的行为。这意味着递增计数:

float f = 0;
while(1)
{
    f +=1;
    printf ("%f\n", f);
}

你会看到打印的值线性增加1,直到饱和23位,指数会变得越来越大。

如果我们的浮点数的基数或基数是 10,我们会看到前 100 (10^2) 个值每 10 个循环增加一次,而接下来的 1000 个增加 100 个(10^3)​​ 值等等。您看到这对应于我们必须进行的*截断**,因为可用数字的数量有限。

使用二进制基数时也会出现同样的现象,只是变化发生在2区间的幂上。

我们到现在讨论的是所谓的浮点数的非规范化形式,通常使用的是对应的规范化。后者只是表示有第 24 位未存储,始终为 1。在平面词中,我们不会对小于 2^24 的数字使用 0 的指数,但我们将它移动(乘以 2)直到 MSbit==1 达到第 24 位,而不是指数被调整为负值,迫使转换将数字移回其原始值。

还记得我们上面讲的指数保留值吗?那么 exponent==0x00 意味着我们有一个非规范化的数字。 exponent==0xff 表示 nan(非数字)或 +/-无穷大,如果 mantissa==0

现在应该清楚了,当我们表达的数字超出了有效(尾数)的 24 位时,我们应该期望实际值的近似值取决于我们与 2^24 的距离。

现在您使用的号码刚好在2^24=16,277,216的边缘:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1| = 16,277,215
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\______ _______/\_____________________ _______________________/
 i       v                              v
 g   exponent                        mantissa
 n

现在加 1 我们有:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0| = 16,277,216
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\__ exponent __/\_________________ mantissa __________________/

请注意,我们已经触发到 1 第 24 位,但是从现在开始我们在 24 位表示之上,并且每个可能的进一步表示都在 2^1=2 的步骤中。只需前进 2 或只能表示偶数(2^1=2 的倍数)。 IE。将我们的低位设置为 1:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1| = 16,277,218
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\__ exponent __/\_________________ mantissa __________________/

再次增加:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0| = 16,277,220
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 s\__ exponent __/\_________________ mantissa __________________/

如您所见,我们无法准确表示 16,277,219。在您的代码中:

// This will print 16777216, because 1 increment isn't enough to
// increase the significant that can express only intervals
// that are > 2^1
printf("16777217 as float is %.1f\n",(float)16777217);
// This will print 16777220, because an increment of 3 on
// the base 16777216=2^24 will trigger an exponent increase rounded
// to the closer exact representation
printf("16777219 as float is %.1f\n",(float)16777219);

如上所述,数字格式的选择必须适合使用,浮点数只是实数的近似表示,我们有责任明确地小心使用正确的类型。

在这种情况下,如果我们需要更高的精度,我们可以使用 double 或整数 long long int

为了完整起见,我将在不可约数的近似表示上添加几句话。此数字不能被 2 的小数整除,因此 float 格式的表示总是不准确,在转换为十进制表示时需要四舍五入到正确的值。

有关详细信息,请参阅:

在线演示小程序: