为什么 `printf` 中没有定义 `float` 的说明符?

69

看起来可能是可以的,因为在C99中有长度修改器可以应用于int: %hhd, %hd, %ld%lld代表signed char, short, longlong long。甚至有一个适用于double的长度修改器: %Lf代表long double

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


%Ld 也不代表 long。它的意思是未定义行为(与传递错误参数相同,会使你之前的 %lf -> long double 对应关系变得未定义)。注意缺乏模式? - autistic
@Seb 不知道,甚至可能是我读错了我的源代码。 - skyking
关于[为什么printf()会将float提升为double?](https://dev59.com/AV4c5IYBdhLWcg3wSIrf)的相关内容。 - Shafik Yaghmour
1
@Seb:我(不明显地?)在谈论scanf说明符,因此下一个子句适用。关于“long float”,请参见C99 Rationale,第42页。猜测可能是决定ANSI委员会是否将显式的%lf添加到printf(在V7中它是未定义行为(通过省略)),以满足当时对“long float”的使用,与scanf对齐,或者两者都适用。 - AntoineL
显示剩余7条评论
8个回答

46
因为在C语言的可变参数函数调用中,任何float类型的参数都会被提升(即转换)为double类型,所以printf得到的是一个double类型的值,并且在其内部实现中使用va_arg(arglist, double)来获取它。
在过去(C89和K&R C),每个float类型的参数都会被转换为double类型。当前标准省略了具有显式原型的固定元数函数的这种提升。这与实现的ABI调用约定有关,详细信息已经解释过了。从实际角度而言,当作为参数传递时,float类型的值通常会被加载到双精度浮点寄存器中,但细节可能会有所不同。可以阅读Linux x86-64 ABI规范 作为一个例子。
此外,没有实际理由为float提供特定的格式控制字符串,因为您可以根据需要调整输出的宽度(例如使用%8.5f),而%hdscanf中比在printf中更有用(几乎是必需的)。除此之外,我猜省略指定float%hf(在printf中被提升为double)的原因是历史性的:起初,C语言是一种系统编程语言,而不是高性能计算(HPC)语言(直到1990年代后期,Fortran在HPC中更受欢迎),因此float并不是非常重要;它(仍然)被认为像short一样,是降低内存消耗的一种方式。今天的浮点数单元(FPU)足够快(在桌面或服务器计算机上),可以避免使用float,除非作为使用更少内存的手段。基本上,您应该相信每个float都会在某个地方(可能在FPU或CPU内部)转换为double
实际上,你的问题可以简化为:为什么printf需要%hd(因为它基本上是无用的,因为当你向其传递一些short时,printf会得到一个int;但是scanf需要它!)。我不知道为什么,但我想在系统编程中可能更有用。
你可以花时间游说下一个ISO C标准,让%hfprintf接受,用于float(在printf调用时升级为double,就像short升级为int一样),当双精度值超出float的界限时具有未定义的行为,并且对称地,让%hfscanf接受,用于float指针。祝你好运。

3
不过,这并不能完全解释 printf 的所有内容。除此之外,还有所有窄类型参数向 int 的自动提升,但仍需要特殊的格式修饰符来处理它们。 - Jens Gustedt
1
但是对于小整数类型格式说明符,C标准涉及升级。例如,参见7.19.6.1 fprintf:hh指定后面的d、i、o、u、x或X转换说明符适用于有符号字符或无符号字符参数(参数将根据整数提升进行升级,但在打印之前其值应转换为有符号字符或无符号字符)。所以我认为这与隐式类型升级无关。 - Lundin
@Lundin 这是一个非常好的观察!这足以消除对这个答案的最后一丝疑虑。 - anatolyg
1
关于“没有实际理由为float提供特定的格式控制字符串”的问题。在某些极端情况下,一些类似"%ha"的东西可能会有用,以不同的方式对齐显示float,例如printf("%a %ha\n", FLT_MAX, FLT_MAX) --> 0x1.fffffep+127 0x0.ffffffp+128,因为第一个例子中讨厌的e只有3个有效位。 - chux - Reinstate Monica
3
printf函数中h和hh修饰符的作用是什么? - phuclv
显示剩余7条评论

15

由于默认参数提升的缘故。

printf()是一个可变参数函数(在其签名中使用...),所有float类型的参数都会被提升为double类型。

C11 §6.5.2.2 函数调用

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

7 函数原型声明符中的省略号符号导致参数类型转换在最后声明的参数后停止。 默认参数提升将作用于尾随参数。


12
不过,这并不能解释一切。所有窄类型都会被提升为int类型,但它们仍然有特殊的格式修饰符。 - Jens Gustedt
7
这段引用并不相关,它只是描述了默认参数提升。人们不能仅仅因为某人引用标准中的一个随机部分就点赞...在点赞之前实际阅读一下它所说的内容。真正相关且指出默认参数提升适用于可变参数函数的部分在6.5.2.2/7中进一步阐述:"函数原型声明中的省略号表示在最后一个声明的参数之后停止进行参数类型转换。默认参数提升将在尾部参数上执行。" - Lundin
3
我已经添加了6.5.2.2/7。第6节解释了如何将“float”提升为默认参数促进中的“double”,第7节解释了“...”如何执行默认参数促进。我的先前回答是不完整的,是的,它仍然是不完整的,但我认为说它是不相关的/随机的是不公平的。 - Yu Hao
5
问题是为什么他们省略了float?你回答说,“因为默认参数提升”,然后引用了在没有函数原型的情况下默认参数提升的工作方式。对我来说这有点随机。 - Lundin
1
@chux 问题仍然是为什么没有一个可以对双精度参数执行相同操作的函数。 - Random832
显示剩余6条评论

10
由于在调用可变参数函数时的默认参数提升,float值在函数调用之前会被隐式转换为double,并且无法将float值传递给printf。既然无法将float值传递给printf,那么就不需要对float值使用显式格式说明符。

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


关于长度修饰符hhh%hhu%hu为这些格式指示符提供了一个明确定义的用例:您可以打印大型unsigned intunsigned short的最低有效字节,例如:
printf("%hhu\n", UINT_MAX); // This will print (unsigned char)  UINT_MAX
printf("%hu\n",  UINT_MAX); // This will print (unsigned short) UINT_MAX

这里并没有明确规定将int转换为charshort会产生什么结果,但至少是实现定义的,这意味着实现必须记录此决策。

按照模式应该是%hf

根据你观察到的模式,%hf应该将超出float范围的值转换回float。然而,从doublefloat的这种缩小转换会导致未定义的行为,而且不存在unsigned float。你看到的模式没有意义。


为了严格正确,%lf并不表示一个long double参数,如果你传递了一个long double参数你将会引发未定义的行为。从文档中可以明确地看到:

l (小写的ell) ... 对于后面的 a, A, e, E, f, F, g, 或 G 转换说明符没有影响。

我很惊讶没有人注意到这一点?%lf表示一个double参数,就像%f一样。如果要打印一个long double,请使用%Lf(大写的ell)。
从现在开始,%lfprintfscanf 中都对应于 doubledouble * 参数... %f 之所以例外,是因为默认参数提升的原因,就像之前提到的那样。
%Ld 并不意味着 long。它意味着 未定义行为

4
当一个short类型的参数作为可变参数传递给printf函数时,它会被提升为一个int类型,而且%hd格式化选项也存在。 - Basile Starynkevitch
4
我的观点是,根据你的论点,没有办法将“short”值传递给“printf”,但是“printf”中存在“%hd”! - Basile Starynkevitch
@BasileStarynkevitch 这样怎么样? - autistic
@BasileStarynkevitch,确实如此,当int值超出范围时,它会以一种实现定义的方式转换回short值。如果有%hf,那么传递类似超出范围的double值只能导致未定义的行为。 - autistic
3
@BasileStarynkevitch 为什么存在%hd?尝试 short x = -72; printf( "%x\n", x ); 并将结果与 short x = -72; printf("%hx\n", x); 进行比较。 - Andrew Henle
显示剩余9条评论

5
从ISO C11标准的6.5.2.2函数调用/ 6/ 7中,讨论了表达式上下文中的函数调用(我强调):
6 / 如果表示被调用函数的表达式具有不包括原型的类型,则对每个参数执行整数提升,并将类型为float的参数提升为double。这些称为默认参数提升。
7 / 如果表示被调用函数的表达式具有包括原型的类型,则将参数隐式转换,就像通过赋值一样,转换为相应参数的类型,将每个参数的类型视为其声明类型的未限定版本。函数原型声明符中的省略号符号导致参数类型转换在最后一个声明的参数之后停止。默认参数提升在尾部参数上执行。
这意味着原型中...后面的任何float参数都会转换为double,并且printf系列调用是以这种方式定义的(7.21.6.11等)。
int fprintf(FILE * restrict stream, const char * restrict format, ...);

因此,由于无法使 printf() 系列调用实际接收浮点数,因此对其进行特殊的格式说明符(或修改符)几乎没有意义。

在标准中,我们可能会有类似这样的内容:%hfdouble 值转换为 float 并以降低的精度显示它。顺便说一句,%hd 也有相同的含义(因为 printf 不接受 short)。 - Basile Starynkevitch
2
但是 printf 也没有接收到一个 short - Basile Starynkevitch
@BasileStarynkevitch,你在重复说同样的话。我已经解释了使用%hd的原因,当你将相同的评论复制/粘贴到我的答案部分时...这与此处提出的问题无关。请注意,通过“问题”我指的是“以问号结尾的一串单词”。 - autistic

2
考虑到scanf为float、double或long double都有不同的格式说明符,我不明白为什么printf和类似函数没有采用相似的方式实现,但这就是C / C++及其标准所采用的方式。
对于push或pop操作的最小大小,可能会根据处理器和当前模式存在问题,但可以通过默认填充来处理,类似于局部变量或结构体变量的默认对齐。当Microsoft从16位编译器转向32/64位编译器时,放弃了对80位(10字节)的long double的支持,现在将long doubledouble(64位/8字节)视为相同。他们本可以按需要填充到12或16字节边界,但却没有这样做。

1
区别在于 scanf 在接受 float 时需要一个指针参数。而调用 scanf 时不会发生相同的类型提升。 - skyking
@skyking - 我理解这一点,但类型提升最初可能已经被指定为处理更多的情况,并且scanf中的格式说明符已经定义好了。 - rcgldr
指针上没有太多类型提升的操作(也许只有将其提升为远指针)。更改目标类型是一个坏主意,首先这意味着当您将指向16位数据的指针作为32位数据解引用时,可能会发生缓冲区溢出。此外,如果您使用中间端序,也无法反过来做。 - skyking
@skyking - 我是指函数参数的类型升级,例如 printf。scanf 需要为每个目标类型指定特定的格式说明符。 - rcgldr

2
阅读C语言的原理,下面是fscanf函数的描述:
引入了C99的一项新功能:hh和ll长度修饰符被添加到C99中。ll支持新的long long int类型。hh添加了将字符类型与所有其他整数类型视为相同的能力;这在实现宏(例如SCNd8)时可能很有用(参见7.18)。
因此,据说添加hh是为了提供对所有新的stdint.h类型的支持。这可能解释了为什么为小整数添加了长度修饰符,但没有为小浮点数添加。
不过这并不能解释为什么C90不一致地具有h而没有hh。就是这样简单,按照C90规定的语言并不总是一致的。后来版本也继承了这种不一致性。

2
当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使得新类型与其他浮点类型在传递给printf或其他可变参数方法时有所不同,因此使得这种自动升级不可行。
因此,当C语言被创建时存在的两种浮点类型可以使用相同的%f格式说明符,但后来创建的long double需要不同的%Lf格式说明符(其中包含一个大写的L)。

2

%hhd%hd%ld%lld被添加到printf中,以使格式字符串更加一致,与scanf相似,尽管它们对于printf来说是多余的,因为默认参数提升已经实现了这个功能。

那么为什么没有为float添加%hf呢?很简单:看一下scanf的行为,float已经有了一个格式说明符,即%f。而double的格式说明符是%lf

C99正是将%lf添加到printf中的。在C99之前,%lf的行为是未定义的(因为标准中没有任何定义)。从C99开始,它是%f的同义词。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接