【问题标题】:C printf %a and %LaC printf %a 和 %La
【发布时间】:2015-05-24 03:34:36
【问题描述】:

我必须用C 重新实现printf(3),而不使用任何可以为我进行转换的函数。

在我理解了感谢你们%a 是如何工作的之后,我以为我已经完成了:How %a conversion work in printf statement?

然后我意识到我不明白rounding是如何完成的,所以我问:C printf float rounding然后我认为在你帮助我之后我已经完成了。

现在%a 完全像官方的一样工作,我认为%La 会和%a 完全一样,但是long double 因为这个人只说:

修饰语 a, A, e, E, f, F, g, G

l (ell) double(忽略,与没有它的行为相同)
L长双

我发现它输出的东西完全不同:'(

double a_double = 0.0001;
long double a_long_double = 0.0001;
printf("%a\n", a_double); #=> 0x1.a36e2eb1c432dp-14
printf("%La\n", a_long_double); #=> 0xd.1b71758e21968p-17

%a 结果总是以1. 开头,现在我完全不明白%La 在做什么。

您能帮我理解将0.0001 转换为0xd.1b71758e21968p-17过程吗?

编辑:我真的不明白的部分是为什么 %a 总是输出以1. 而不是%La 开头的东西?

EDIT2:更准确地说:为什么%a 选择输出1. ...p-14%La 选择输出d. ...p-17

【问题讨论】:

  • @Wintermute:但无论如何整个输出都是四舍五入的......打印的两个数字相同,但选择的指数不同。
  • @BillLynch 是的,正是我的问题!
  • @Wintermute:这两个数字之间没有区别(除了类型):如果您将一个表示移三位,您将得到另一个。
  • @BillLynch:您的观点是正确的,只是挑剔: %a 的真正目的是不对要输出的数字进行四舍五入!
  • @Wintermute:如果您不了解实际发生的情况,请不要告诉人们这是“浮点舍入错误”。这里没有舍入误差;事实上,格式中根本没有四舍五入。

标签: c hex printf type-conversion


【解决方案1】:

你没有说你在这里工作的是什么类型的机器,但是从你得到一个0xd....的事实来看,%La 的结果意味着这是一台机器,其中long double 是一个 unnormalized 浮点类型(例如 8087 上的 80 位浮点数)。使用 normalized 浮点数,您将始终得到一个以 0x1.... 开头的结果,正如您所猜测的那样,但是对于非标准化浮点数,有多个相同数字的表示,这就是您的看到这里了。

%a 转换规范的全部要点是允许将二进制浮点数打印为文本,以便以后可以使用 scanf 读回,这将在具有相同的浮点表示。

【讨论】:

  • 哦,好吧,我不知道这会有什么影响,我使用的是 OS X 10.10.2
  • 在 C 规范中看不到第一个数字必须是 0x1 "表示浮点数的双参数转换为样式 [−]0xh.hhhh p±d, ,其中在小数点字符之前有一个十六进制数字(如果参数是规范化的浮点数,则为非零,否则未指定)..." C11dr §7.21.6.1 8
  • 对于普通数字,80 位扩展双精度数的前导位始终为 1。这个前导位是明确的这一事实不会改变它应该被打印的方式。不过,64 可被 4 整除这一事实使得打印总共不超过 16 个十六进制数字很诱人。
  • C 规范中的脚注专门针对非 0 或 1 的前导数字:“二进制实现可以选择小数点字符左侧的十六进制数字,以便后续数字与半字节对齐(4 位)边界”。这与此答案的“使用标准化浮点数,您将始终得到以 0x1 开头的结果”相矛盾。
  • @chux:虽然规范没有正式要求它,但这将是一个相当奇怪的实现,它需要额外的工作来为具有隐藏 1 位的浮点格式生成其他东西。
【解决方案2】:

任何(有限、非零)浮点数都有四种不同(且有效)的十六进制浮点表示:

  1. 最高十六进制数字在 8 和 F 之间的一个
  2. 另一个最高的十六进制数字在 4 到 7 之间
  3. 另一个最高十六进制数字是 2 或 3
  4. 另一个最高十六进制数字是1

并且它们具有连续递增的指数值。您可以使用右移从一个传递到另一个;这个过程可能会向右增加另一个数字; C 标准中没有关于您应该使用哪种表示来进行%a 转换的限制。

对于 IEEE float(24 位有效位)或通常的双扩展(64 位有效位,与 i386 Linux 上的 long double 一样),由于位数可以被 4 整除,因此习惯上使用规范化数字的第一种形式,因为它是使用最少的十六进制数字来完全表示数字的形式(分别在 十六进制. 之后的 5 位和 15 位数字)。

另一方面,对于 IEEE double(53 位有效位),在点 . 之后使用第四种形式和 13 个十六进制数字看起来更好:因此所有显示的数字实际上都表示数据。 IEEE quad(正式为 binary128,113 位有效位)也会发生同样的情况,点后有 28 个十六进制数字。

这些表示还具有与数字的内存表示相匹配的良好属性(这就是 C99 标准中的脚注引导实现这样做的原因。)

如果我们现在查看您的问题,第一个示例 (double) 符合上述准则,第一个数字为 1,点后为 13 个十六进制数字。

第二个例子更狡猾。首先将double 常量存储到long double 变量中:根据编译器的不同,常量可能会舍入到double 的精度(就像这里似乎所做的那样。)这意味着第53 位之后的位将全为零。然后使用%La转换,同样按照上面的指导,因此选择第一个表示,以D开头(二进制的1101xxx,与翻译成11010xxx的1.A进行比较);这里也只有 13 个十六进制数字,因为编译器丢弃了打印不必要的零(由于四舍五入)。

请注意,您不能将printf%a 转换为float

【讨论】:

  • 我不知道!非常感谢!顺便说一句,答案很好。
  • 出色的答案! (预计 53 日 --> 53 日)。
【解决方案3】:

目前尚不清楚结果的哪个方面让您感到惊讶。如果是指数和数字的差异,那只是因为归一化差。请注意,对于第一个结果,您会在小数点之前得到一个 1,而对于第二个结果,您会在它之前得到一个 d。将d 右移 3 位,得到1,低位 (101) 移动到小数点之后的下一个位置,得到a,正如预期的那样。

至于0.0001如何转化为0xd.1b71758e21968p-17,只需要找到最接近十进制数0.0001的二进制浮点值即可。在某种程度上涉及如何有效地完成此操作的机制,但概念很简单。

【讨论】:

  • 为什么输出的东西总是以'1'开头。 (%a) 而不是另一个 (%La) ?
  • @ItsASecret binary64 有 52 个显式位和一个隐式前导位。 52 可以被 4 整除,因此将其打印为 1.xxxxxxxxxxxxx 以外的任何内容都是浪费和烦人的。另一方面,您正在处理的扩展双精度数总共有 64 位的有效位,并且没有用隐式位编码。为避免溢出到其他字符,大多数printf 实现将其打印为x.xxxxxxxxxxxxxxx
  • @ItsASecret:我认为这是一个实现缺陷。从在给定精度规范中呈现最有用信息的角度来看,您希望小数点之前的数字始终在8..f 范围内。打印 1 会导致精度无缘无故损失。但这是一个有效的实现。
  • @PascalCuoq:这仅适用于打印精度足以准确表示值的情况(在没有明确指定精度的情况下为真)。如果调用者限制了精度,则不宜使用前导 1。
  • @R.. 要显示 53 位,您总是“无缘无故”地丢失了某个端;使用右对齐表示的好处,因此在左端松散始终是 1.,是匹配内存中的表示。作为调试工具,我认为值得。如果您是手动编码,它还可以使实现稍微容易一些,从调试的角度来看,这也是一个好点(我不喜欢追逐 libc 错误。)
猜你喜欢
  • 1970-01-01
  • 2011-09-04
  • 2012-08-27
  • 2017-08-22
  • 2019-07-27
  • 2013-09-02
  • 2017-02-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多