【问题标题】:Why wasn't a specifier for `float` defined in `printf`?为什么没有在`printf`中定义`float`的说明符?
【发布时间】:2015-12-07 16:31:01
【问题描述】:

看起来可能是这样,有(至少在 C99 中)长度修饰符可以应用于 int%hhd%hd%ld%lld 意思是 signed charshortlonglong long。甚至还有一个适用于double 的长度修饰符:%Lf 表示long double

问题是为什么他们省略了float?按照这种模式,它可能是%hf

【问题讨论】:

  • @Seb 我从“错误”的地方得到了格式说明符。一些实现似乎接受%lflong double,但标准为此指定了%Lf。我已经更新了问题。
  • ... 和 %Ld 也不意味着 long。这意味着undefined behaviour(与传递错误参数相同的部分和段落会使您之前的@​​987654344@ -> long double 对应未定义)。注意到缺少模式了吗?
  • @skyking 那会是哪个实现?
  • @Seb:我(不明显?)在谈论scanf 说明符,所以下一个子条款适用。关于long float,参见例如C99 Rationale,第42 页。会是猜测可能是决定ANSI 委员会是否将明确的%lf 添加到printf(它是undefined behaviour (by omission) in V7 ) 来满足long float 的现有用法,与scanf 保持一致,或者两者都适用。

标签: c floating-point printf rationale c-standard-library


【解决方案1】:

因为在 C 可变参数函数调用中,任何float 参数都被提升(即转换)为double,所以printf 得到一个double 并将使用va_arg(arglist, double) 将其放入其实现中。

过去(C89 和 K&R C)每个 float 参数都转换为 double。当前标准省略了对具有显式原型的固定数量函数的这种提升。它与实现的ABIcalling conventions 相关(并在其中解释了详细信息)。实际上,float 值在作为参数传递时通常会加载到双浮点寄存器中,但细节可能会有所不同。以 Linux x86-64 ABI specification 为例。

此外,没有实际理由为float 提供特定的格式控制字符串,因为您可以根据需要调整输出的宽度(例如使用%8.5f),而%hd 更有用(几乎必要)在scanf 中比在printf

除此之外,我猜是原因(省略 %hf 指定 float - 在调用者中提升为 doubleprintf 中)是历史原因:起初,C 是一种系统编程语言,而不是 HPC 语言(也许直到 1990 年代后期,HPC 还是首选 Fortran),float 不是很重要;它曾经(现在仍然)被认为是 short,一种降低内存消耗的方法。今天的 FPU 足够快(在台式机或服务器计算机上),可以避免使用 float,除非作为使用更少内存的手段。您基本上应该相信每个float 都在某个地方(可能 FPU 或CPU 内部)转换为double

实际上,您的问题可能被解释为:为什么%hd 存在于printf (它基本上是无用的,因为printf 在您传递一些short 时得到int;但是@987654352 @需要它!)。我不知道为什么,但我想它可能比在系统编程中更有用。

您可以花时间游说下一个 ISO C 标准,让printf 接受float%hf(在printf 调用时升级为double,例如short-s 升级为@987654359 @),当双精度值超出 float-s 的界限时,具有未定义的行为,并且 scanf 对称地接受 %hf 以获取 float 指针。祝你好运。

【讨论】:

  • 不过,这并不能解释 printf 的一切。还有所有窄类型的参数提升到int,但仍然有特殊的格式修饰符。
  • 但对于小整数类型格式说明符,C 标准地址提升。参见例如 7.19.6.1 fprintf:hh Specifies that a following d, i, o, u, x, or X conversion specifier applies to a signed char or unsigned char argument (the argument will have been promoted according to the integer promotions, but its value shall be converted to signed char or unsigned char before printing);。所以我不认为这与隐式类型提升有任何关系。
  • @Lundin 这是一个很好的观察!从这个答案中消除最后一点怀疑就足够了。
  • 关于“没有实际理由为float 提供特定的格式控制字符串”。在极端情况下,某种"%ha" 可能有助于以不同的方式显示float,因为printf("%a %ha\n", FLT_MAX, FLT_MAX) --> 0x1.fffffep+127 0x0.ffffffp+128 在第一种情况下讨厌的e 只有3 个有效位。
【解决方案2】:

由于默认参数提升

printf() 是一个可变参数函数(... 在其签名中),所有float 参数都被提升为double

C11 §6.5.2.2 函数调用

6 如果表示被调用函数的表达式的类型不包含原型,则对每个参数执行整数提升,并且类型为 float 的参数提升为 double。这些称为默认参数提升。

7 函数原型声明器中的省略号表示 参数类型转换在最后一个声明的参数之后停止。默认参数提升是在尾随参数上执行的。

【讨论】:

  • 但这并不能解释一切。还有所有窄类型的参数提升到int,但仍然有它们的特殊格式修饰符。
  • 这句话也无关紧要,它只是描述了默认的参数提升。人们不得不停止投票,只是因为有人引用了标准的随机部分......实际上在你做之前先阅读它所说的内容。在 6.5.2.2/7 中,相关的部分实际上表明默认参数提升适用于可变参数函数:“函数原型声明器中的省略号表示法会导致参数类型转换在最后一个声明的参数之后停止。默认参数提升是在尾随参数上执行的。"
  • @Lundin 我添加了 6.5.2.2/7。第 6 节解释了在默认参数提升中 float 如何提升为 double,第 7 节解释了 ... 如何执行默认参数提升。我之前的回答是不完整的,是的,现在仍然是,但我认为说它不相关/随机是不公平的。
  • "问题是为什么他们省略了浮动?"您回答“因为默认参数提升”,然后引用了在没有函数原型的情况下默认参数提升如何工作的引述。这对我来说是相当随机的。
  • @chux 问题仍然是为什么没有人对双重参数做同样的事情。
【解决方案3】:

由于调用可变参数函数时默认参数提升,float值在函数调用前隐式转换为double无法传递@987654329 @值为printf。由于无法将float 值传递给printf,因此不需要为float 值提供显式格式说明符。

话虽如此,AntoineL 在评论中提出了一个有趣的观点,即%lf(目前在scanf 中用于对应参数类型double *)可能曾经代表“long float”,根据the C99 rationale 的第 42 页,这是 C89 之前的类型同义词。按照这种逻辑,%f 旨在代表已转换为 doublefloat 值可能是有道理的。


关于 hhh 长度修饰符,%hhu%hu 为这些格式说明符提供了一个定义明确的用例:您可以打印大 unsigned int 或 @ 的最低有效字节987654346@ 没有演员表,例如:

printf("%hhu\n", UINT_MAX); // This will print (unsigned char)  UINT_MAX
printf("%hu\n",  UINT_MAX); // This will print (unsigned short) UINT_MAX

intcharshort 的缩小转换将导致什么没有特别明确的定义,但它至少是实现定义的,这意味着实现是需要实际记录此决定。

按照应该是%hf的模式。

按照您观察到的模式,%hf 应该将float 范围之外的值转换回float。然而,那种从doublefloat results in undefined behaviour 的缩小转换,并没有unsigned float 这样的东西。你看到的模式没有意义。


为了正式正确,%lf 不表示 long double 参数,如果您要传递 long double 参数 you would be invoking undefined behaviourthe documentation 明确指出:

l(埃尔) ... 对以下 aAeEfFgG 转换说明符没有影响。

我很惊讶没有其他人对此有所了解? %lf 表示 double 参数,就像 %f 一样。如果要打印 long double,请使用 %Lf(大写 ell)。

从今以后,%lf 对于 printfscanf 对应于 doubledouble * 参数应该是有意义的... %f 是例外的,只是因为默认参数提升,原因前面提到过。

... 和 %Ld 也不意味着 long。这意味着undefined behaviour

【讨论】:

  • 但是当short 用作printf%hd 的可变参数时,short 被提升为int 存在
  • @BasileStarynkevitch OP 的问题是“他们为什么省略 float?”......而不是“他们为什么不省略 short?”。你的担心对我来说似乎无关紧要,但如果你真的想要我,只要吠叫,我会解释为什么会这样。
  • 我的意思是,按照你的论点,没有办法将short 值传递给printf,但是%hd 存在printf
  • @BasileStarynkevitch 怎么样?
  • @BasileStarynkevitch 为什么%hd 存在?尝试short x = -72; printf( "%x\n", x ); 并将结果与​​short x = -72; printf("%hx\n", x); 进行比较
【解决方案4】:

根据 ISO C11 标准,6.5.2.2 Function calls /6/7,讨论表达式上下文中的函数调用(我的重点):

6/ 如果表示被调用函数的表达式的类型不包含原型,则对每个参数执行整数提升,并且 浮点类型的参数提升为双精度。这些称为默认参数提升。

7/ 如果表示被调用函数的表达式具有包含原型的类型,则参数将隐式转换为相应参数的类型,就像通过赋值一样,将每个参数的类型作为其声明类型的非限定版本。函数原型声明器中的省略号表示 参数类型转换在最后一个声明的参数之后停止。 默认参数提升是在尾随参数上执行的。

这意味着原型中... 之后的任何float 参数都将转换为double,并且printf 系列调用以这种方式定义(7.21.6.11 et seq):

int fprintf(FILE * restrict stream, const char * restrict format, ...);

因此,由于printf()-family 调用实际上无法接收一个浮点数,因此为其设置一个特殊的格式说明符(或修饰符)毫无意义。

【讨论】:

  • 我们在标准中可能有类似:%hfdouble 值转换为float 并以降低的精度显示它。顺便说一句,%hd 具有相同的含义(因为printf 也没有得到short
  • 但是printf 也没有收到short
  • @BasileStarynkevitch 你正在播放破唱片。当您将相同的评论复制/粘贴到我的答案部分时,我已经解释了%hd 背后的基本原理......这与这里提出的问题无关。请注意,question 我的意思是“以问号结尾的字符串”。
【解决方案5】:

阅读C原理,在fscanf下面,可以找到以下内容:

C99 的一个新特性:hh 和 ll 长度修饰符被添加到 C99。 ll 支持新的 long long int 类型。 hh 增加了能力 将字符类型视为与所有其他整数类型相同;这可以是 在实现诸如 SCNd8 之类的宏时很有用(参见 7.18)。

所以据说添加了hh 是为了为所有新的stdint.h 类型提供支持。这可以解释为什么为小整数添加了长度修饰符,而不是为小浮点数添加了长度修饰符。

它没有解释为什么 C90 不一致地有h 但没有hh。 C90 中指定的语言并不总是一致的,就这么简单。并且后来的版本继承了这种不一致。

【讨论】:

    【解决方案6】:

    当 C 被发明时,所有浮点值在用于计算或传递给函数(包括)printf 之前都被转换为通用类型(即double),因此不需要printf区分浮点类型。

    为了提高算术效率和准确性,IEEE-754 浮点标准定义了一个 80 位类型,它比普通的 64 位 double 更大,但可以更快地处理。其目的是给定一个像a=b+c+d; 这样的表达式,将所有内容转换为 80 位类型,将三个 80 位数字相加,然后将结果转换为 64 位类型,会更快更准确,而不是将总和 (b+c) 计算为 64 位类型,然后将其添加到 d

    为了支持新类型,ANSI C 定义了一个新类型 long double,其实现可以引用新的 80 位类型或 64 位 double。不幸的是,尽管 IEEE-754 80 位类型的目的是让所有值自动提升为新类型,就像它们被提升为 double 的方式一样,ANSI 还是做到了,所以新的type 被传递给printf 或与其他浮点类型不同的其他可变参数方法,从而使这种自动提升变得站不住脚。

    因此,创建 C 时存在的两种浮点类型都可以使用相同的 %f 格式说明符,但之后创建的 long double 需要不同的 %Lf 格式说明符(带有 大写 L)。

    【讨论】:

      【解决方案7】:

      %hhd%hd%ld%lld 添加到 printf 以使格式字符串与 scanf 更加一致,即使它们对于 printf 是多余的,因为默认参数促销活动。

      那么为什么不为float 添加%hf?这很简单:查看scanf 的行为,float 已经有了格式说明符。这是%fdouble 的格式说明符是 %lf

      %lf 正是 C​​99 添加到 printf 的内容。在 C99 之前,%lf 的行为是未定义的(通过省略标准中的任何定义)。从 C99 开始,它是 %f 的同义词。

      【讨论】:

        【解决方案8】:

        考虑到scanf 对 float、double 或 long double 有单独的格式说明符,我不明白为什么 printf 和类似的函数没有以类似的方式实现,但这就是 C / C++ 和标准的实现方式结束了。

        根据处理器和当前模式,推送或弹出操作的最小大小可能存在问题,但这可以通过默认填充处理,类似于结构中局部变量或变量的默认对齐方式。当从 16 位编译器升级到 32 / 64 位编译器时,Microsoft 放弃了对 80 位(10 字节)long doubles 的支持,现在将long doubles 视为与doubles(64 位/8 字节)相同。他们本可以根据需要将它们填充到 12 或 16 字节边界,但没有这样做。

        【讨论】:

        • 不同之处在于scanf 在接受float 时需要一个指针参数。调用scanf时不会发生相同类型的提升。
        • @skyking - 我明白这一点,但最初可以指定类型提升来处理更多情况,并且已经定义了来自 scanf 的格式说明符。
        • 没有太多可以对指针进行的类型提升(可能不仅仅是将其提升为远指针)。更改目标的类型是一个坏主意,首先这意味着当您取消引用指向 16 位数据的指针时,您可能会遇到缓冲区溢出,就好像它是 32 位一样。此外,如果你有中间端,你也不能这样做。
        • @skyking - 我的意思是对 printf 等函数的参数进行类型提升。 scanf 需要为每个目标类型指定特定的格式说明符。
        猜你喜欢
        • 1970-01-01
        • 2013-06-22
        • 2013-06-22
        • 1970-01-01
        • 2020-07-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-07
        相关资源
        最近更新 更多