%a 转换如何在 printf 语句中工作?

How %a conversion work in printf statement?

也许这不属于 SO,但我不知道还有什么地方。

我必须用 C 重新实现 printf(3),而不使用任何可以为我进行转换的函数,我快完成了,但我仍然停留在 %a,我真的不明白这里发生了什么,例如:

printf("%a\n", 3.0); //#=> 0x1.8p+1
printf("%a\n", 3.1); //#=> 0x1.8cccccccccccdp+1
printf("%a\n", 3.2); //#=> 0x1.999999999999ap+1
printf("%a\n", 3.3); //#=> 0x1.a666666666666p+1
printf("%a\n", 3.4); //#=> 0x1.b333333333333p+1
printf("%a\n", 3.5); //#=> 0x1.cp+1
printf("%a\n", 3.6); //#=> 0x1.ccccccccccccdp+1

当然我读过那个人说:

The double argument is rounded and converted to hexadecimal notation in the style[-]0xh.hhhp[+-]d, where the number of digits after the hexadecimal-point character is equal to the precision specification.

但这并没有真正帮助我不理解 3.2 转换为 1.999999999999ap+1

的过程

我不需要任何代码,但更需要解释。

PS:如果这不是回答这个问题的地方,你能告诉我正确的地方吗?

编辑:虽然 @juhist 答案适用于 >= 1.0 的数字,但它没有解释如何获得 0.01.0 之间的数字的结果:

printf("%a\n", 0.01); //#=> 0x1.47ae147ae147bp-7
printf("%a\n", 0.1);  //#=> 0x1.999999999999ap-4
printf("%a\n", 0.2);  //#=> 0x1.999999999999ap-3
printf("%a\n", 0.3);  //#=> 0x1.3333333333333p-2
printf("%a\n", 0.4);  //#=> 0x1.999999999999ap-2
printf("%a\n", 0.5);  //#=> 0x1p-1
printf("%a\n", 0.6);  //#=> 0x1.3333333333333p-1

此外,我真的很想在这个 The "a" near the end occurs due to limited floating point calculation precision 上关于转换的精确度:printf("%a\n", 3.2); //#=> 0x1.999999999999ap+1

EDIT2:现在最后一个谜是解释为什么在这种情况下:

printf("%a\n", 0.1); //#=> 0x1.999999999999ap-4

最后的 9 变成 a 在这种情况下:

printf("%a\n", 0.3); //#=> 0x1.3333333333333p-2

最后 3 保持 3?

这个问题很容易回答。

显而易见的解释是 (1 + 9.0/16 + 9.0/(16*16) + 9.0/(16*16*16) + 9.0/(16*16*16*16) + ...)*2 = 3.2

您可以通过获取前五个术语并将其写入 Python 解释器来轻松验证这一点: (1 + 9.0/16 + 9.0/(16*16) + 9.0/(16*16*16) + 9.0/(16*16*16*16))*2

答案是3.199981689453125.

为什么最后是 2?当然,因为 (1<<1) = 2 而 "p" 后面的数字是 +1。如果您有 +2,那么您将使用 (1<<2) = 4 作为乘数。

编辑:好的,所以需要一个相反的算法。

首先求(3.2/(2^x))在1到2之间的偏移量x。在 这种情况下,3.2/(2^1) = 3.2/2 = 1.6.

然后,输出“1”。并从结果中减去 1,得到 0.6

然后,将结果乘以16,取整数部分。产品是9.6。 输出为“9”。

然后,从结果中减去输出。 9.6 - 9 = 0.6.

重复:将结果乘以 16 并取整数部分。该产品是 9.6.输出为“9”。

再次从结果中减去输出。 9.6 - 9 = 0.6

重复这些过程。无穷无尽,如果你想要完全扩展,但在 练习你会在某处停止迭代。最后,你输出 "p" 和 "+1" 因为幂是 +1。由于浮点计算精度有限,出现接近尾声的"a"。

另一个编辑:要完全了解有限精度会产生什么影响,您可以阅读 "What Every Computer Scientist Should Know About Floating-Point Arithmetic",这是一篇非常有名的论文,有在线版本,例如那里:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

数字 0.1 的算法示例:我们发现 0.1/(2^(-4)) = 0.1*2^4 = 0.1*16 = 1.6 介于 1 和 2 之间,因此我们选择功率-4。所以,我们输出“1”。然后计算(1.6-1)=0.6,乘以16:16*0.6 = 9.6。所以,我们输出“9”,减去 9.6 - 9 = 0.6,再乘以 16,得到另一个“9”。等等。因此,在无限浮点精度的情况下,答案是“1.9999999....p-4”。但是,由于精度有限,最后一个字母似乎是 "a"。因此,该算法也适用于负幂,但您必须注意除以负幂与乘以正幂相同。因此,在选择给出 1 和 2 之间的值的幂时,您还需要考虑负数。

首先你需要知道表示0xh.hhhh p±d,是什么意思?我们以十六进制常量0x1.99999ap+1.
为例来理解 小数点前的数字1为十六进制数字 它后面的十六进制数字 (99999a) 等于精度。 0x 是十六进制引入符,p 是指数字段。指数是一个十进制数,表示有效部分乘以 2 的幂。

所以,当0x1.99999ap+1乘以21时,它会被转换成十进制的3.2 .回想一下 1.55e+1 如何转换为十进制的 15.500000。类似的事情正在这里发生。

现在您需要了解 0x1.99999ap+13.2 转换背后的数学原理。这将按如下方式进行

1*160 + 9*16-1 + 9*16-2 + 9*16-3 + 9*16-4 + 9*16-5 + 10*16-1

(十进制)等于

 1.60000002384185791015625  

您最多只需要 1 个精度。因此,将 1.6 乘以 21。这将给出 3.2

要逆向执行上述过程,您需要找到 2 的幂,浮点数将除以该幂以获得小数点前的数字 1。之后使用连续乘法将小数部分更改为十六进制分数。进行如下:

  1. 3.2/21 = 1.6
  2. Take integral part from 1.6 to obtain the hex-digit 1 before decimal point.
  3. Multiply .6 by 16. The the integral part obtained will becomes a numeral in the hexadecimal fraction. Repeat this step with the obtained fractional part to the desired precision (6 is default).

    • .6 * 16 = 9.6 ---> Integral part = 9 Fractional part = .6
    • .6 * 16 = 9.6 ---> Integral part = 9 Fractional part = .6
    • .6 * 16 = 9.6 ---> Integral part = 9 Fractional part = .6
    • .6 * 16 = 9.6 ---> Integral part = 9 Fractional part = .6
    • .6 * 16 = 9.6 ---> Integral part = 9 Fractional part = .6
    • .6 * 16 = 9.6 ---> Integral part = 9 Fractional part = .6

所以十六进制小数会变成.999999 现在结合十六进制指示符 0x、小数点前的十六进制数字和与指数字段一起获得的十六进制分数来得到结果。

3.210 = 0x1.999999p+116

同样可以得到小于1.0的数字的十六进制浮点数,例如0.01。在这种情况下,要获得小数点前的十六进制数字 1,您需要将其除以 2 的幂。因为128(25)乘以0.01后的整数部分变成1,128*.01 = 1.28。这意味着您需要将 0.01 乘以 1/2-5 或者您可以说您需要将其除以 2-5得到 1.28。现在应用上述步骤 2 和 3。