printf("%x",1)是否会引发未定义行为?

37
根据C标准(6.5.2.2第6段):
如果表示所调用函数的表达式具有不包括原型的类型,则对每个参数执行整数提升,如果参数的类型为float,则将其提升为double。这些被称为默认参数提升。如果参数的数量与参数的数量不相等,则行为未定义。如果使用包含原型的类型定义函数,且原型以省略号 (,...) 结尾或在推广后的参数类型与参数类型不兼容,则行为未定义。如果使用不包含原型的类型定义函数,并且推广后的参数类型与推广后的参数类型不兼容,则行为未定义,但以下情况除外:
一个提升的类型是有符号整数类型,另一个提升的类型是相应的无符号整数类型,并且该值在两种类型中均可表示; 这两种类型都是限定或未限定版本的字符类型或void指针。
因此,通常情况下,只要传递的值适合于两种类型,就可以将int传递给期望unsigned int(反之亦然)的可变参数函数,但是,printf的规范(7.19.6.1第9段)如下:
如果转换说明符无效,则行为未定义。如果任何参数不是相应转换说明符的正确类型,则行为未定义。
没有为signed/unsigned不匹配做出例外。
这是否意味着printf(“%x”,1)会引发未定义的行为?

2
对这个问题感兴趣的人可能(或可能不)会对这个相关问题感兴趣:https://dev59.com/Hm455IYBdhLWcg3wAvXQ - Michael Burr
2
那么printf("%d",(char)1);呢?printf的描述并没有说它必须是正确类型的整数提升后的参数,而是说参数本身必须是正确类型。我们应该得出结论,它也是6.5.2.2/6这部分的一个例外吗? - Steve Jessop
1
顺便说一句,我认为你的引用不足以说明问题,因为如果没有原型,调用printf是未定义的行为,而你的引用涉及到在没有原型的情况下进行的调用。然而,根据6.5.2.2/7,相同的参数提升适用于varargs的参数,尽管这并没有说明有关有符号/无符号兼容性的内容。所以也许你是完全正确的,有符号/无符号兼容性只适用于没有原型的调用,而不适用于varargs调用,更不用说特定的printf了。 - Steve Jessop
4
如果我没错的话,我认为这是标准上的缺陷,可能需要修正。这种严格的解释会导致大量代码不正确,并需要大量丑陋而毫无意义的转换。 - R.. GitHub STOP HELPING ICE
1
表面上看,unsigned short x = 1; printf("%hu\n", x); 由于整数提升引入的无符号/有符号不匹配问题,也可能会出现未定义行为,尽管大多数人阅读它时可能不会预料到这种情况。 - dbush
显示剩余8条评论
6个回答

17

我认为这在技术上是未定义的,因为对于%x而言“正确类型”被指定为unsigned int - 正如你所指出的,这里没有针对有符号/无符号不匹配的特殊情况。

printf的规则是针对更具体的情况而制定的,因此会覆盖通用情况的规则(另一个例子是,在一般情况下,将NULL传递给期望const char *参数的函数是允许的,但将其传递给strlen()是未定义的行为)。

我说“技术上”,是因为我相信,鉴于标准中的其他限制,实现必须有意扭曲才能在这种情况下引发问题。


我认为这种解释意味着标准打算让 printf 函数族的参数以不同于其他可变参数函数的方式传递,这是没有意义的。 - Chris Lutz
2
@Chris Lutz:这种解释并不涉及标准的意图,它只是提出了一条关于标准实际规范措辞影响的论点。 - caf
将NULL传递给strlen()是未定义行为。但这并不是在做一个特殊情况;解引用空指针是未定义行为,而strlen解引用它所接收的指针。将null传递给strlen()的行为并不是未定义的,尽管它确实会导致后续的UB-causing操作。 - Karl Knechtel
2
@Karl: 其实,将NULL传递给strlen函数是未定义行为。这是因为标准库函数的定义是根据其行为而不是C实现来确定的。请参考7.4.1/1:「如果函数的参数具有无效值(例如函数域之外的值、程序地址空间之外的指针、空指针或非可修改存储的指针,当相应的参数没有const限定时),或者一个函数预期的类型(在提升后)与变量数量不定的函数不符合,则行为是未定义的。」 - R.. GitHub STOP HELPING ICE
1
在过去的几年中,实现逐渐变得故意扭曲起来。我认为除非有人编写一个旨在建立有益规范行为并要求扭曲编译器记录与规范不符之处的标准,否则用C进行编程将不安全。 - supercat

8
不可以,因为%x格式化的是无符号整数,而常量表达式1的类型是int,但其值可以表示为无符号整数。该操作不是未定义行为。

2
它可以格式化两者。:) 可变参数规范覆盖 printf 规范,前者允许在需要 unsigned int 的地方使用 int。 - Jonathan Grynspan
1
实际上,"%x"需要一个"unsigned int"而不是"int"参数。R.想知道他从标准中引用的各种细节是否意味着这在技术上是未定义的行为。 - Michael Burr
5
6.5.2.2 一般定义了可变参数函数的行为,但是 7.19.6.1 却说除非类型与格式说明符匹配,否则行为未定义。看起来这一段应该被省略或者修正以提到有符号/无符号不匹配的例外情况,如果这是意图的话。 - R.. GitHub STOP HELPING ICE
11
默认的参数提升通常不会导致将 int 参数转换为 unsigned int,因此 1 必须能够表示为 unsigned int 是无关紧要的。如果 printf 能够保证使用 va_arg 宏,那么你可以期望在 7.12.1.1 中的异常情况成立,但这不是必需的。在默认参数提升之后,参数的类型仍然是 int 而不是 unsigned int,并且(正如其他人所说)7.19.6.1 明确规定:"如果任何参数与相应的格式说明符不是正确的类型,则行为是未定义的。" - CB Bailey
3
就我个人而言,我认为这段代码应该是合法的,但标准没有明确定义它,我认为标准在这里存在缺陷。通过查看“l”修饰符的说明,可以明显看出一个明显的不一致性,即"%lx"可能对应于参数1L-1L - M.M
显示剩余5条评论

4
由于与将指针重新解释为带有相反符号的补码类型相同的原因,这是未定义的行为。不幸的是,在两个方向上都不允许这样做,因为在一个类型中有效的表示可能在另一个类型中是一个陷阱实现。
我唯一能看到从有符号到无符号的重新解释中可能会出现陷阱表示的原因是,符号表示的这种变态情况下,无符号类型只是掩盖了符号位。不幸的是,根据标准6.2.6.2的规定,这样的事情是被允许的。在这样的架构上,有符号类型的所有负值可能成为无符号类型的陷阱表示。
在您的示例情况中,这更加奇怪,因为使1成为无符号类型的陷阱表示是不被允许的。因此,要使其成为一个“真正”的示例,您必须使用-1来提问。
我认为人们编写C编译器的任何架构都不再具有这些功能,因此,如果新版本的标准能够废除这种令人讨厌的情况,生活肯定会变得更加容易。

3
我不相信这是符合标准的。据我所知,可用有符号和无符号类型表示的值必须具有相同的表示形式。请注意,“类型的表示”中的别名规则明确允许对不匹配符号类型进行访问。 - R.. GitHub STOP HELPING ICE
@R.. 只需在标准中查找即可。它明确指出有符号类型的值位数小于或等于无符号类型的值位数。特别地,负有符号值可能是无符号类型的陷阱表示也是允许的。对于别名规则,您可能是正确的。因此,这个需要一个缺陷报告。 - Jens Gustedt
我同意你刚才说的话。然而,这并不与有符号类型的正值必须在表示上与无符号类型的相同值达成一致的要求相矛盾 - 我认为这是有意而为之的,并且即使没有明确说明,其他条件也暗示了这一要求。 - R.. GitHub STOP HELPING ICE
1
@R.. 实际上明确指出,只要正值适合两种类型的表示方式,它们必须具有相同的表示方式。我已经相应地更正了我的答案。 - Jens Gustedt

0

我认为它是未定义的。具有可变长度参数列表的函数在接受参数时没有隐式转换,因此当传递给printf()时,1不会被强制转换为unsigned int,从而导致未定义行为。


@M.M 对不起,我的意思是“隐式转换”。 - nalzok

0

简而言之,这不是未定义行为。

正如n. '代词' m.在答案中指出的那样,C标准规定,有符号整数类型的所有非负值与相应的无符号类型具有完全相同的表示形式,因此只要该值在两种类型的范围内,就可以互换使用。

来自C99标准6.2.5类型-第9段和脚注31:

9 有符号整数类型的非负值范围是相应无符号整数类型的子范围,并且每种类型中相同值的表示方式相同。31)

31)相同的表示和对齐要求意味着可作为函数参数、函数返回值和联合成员互换使用。

在C11标准的6.2.5类型-第9段和脚注41中也有完全相同的文本。


-1
标准的作者通常不会试图在每个可能的角落情况下明确规定行为,特别是当存在一个明显正确的行为,所有实现都共享,并且没有理由期望任何实现做其他事情时。尽管标准明确要求有符号和无符号类型对于适合两者的值具有匹配的内存表示,但实现可以在将它们传递给可变参数函数时以不同的方式传递。标准并不禁止这种行为,但我没有看到作者有意允许这种行为的证据。最有可能的是,他们只是没有考虑过这种可能性,因为没有实现曾经(至少就我所知)以这种方式工作过。
如果代码在有符号值上使用%x,则对于一个消毒实现来说,抱怨可能是合理的,尽管质量良好的消毒实现还应该提供一个选项来默默接受这样的代码。对于正常的实现来说,没有理由做任何其他事情,除了将传递的值处理为无符号或在诊断/消毒模式下使用时抱怨。虽然标准可能禁止实现将使用%x的有符号值的任何代码视为不可达,但任何认为实现应该利用这种自由的人都应该被认为是白痴。

专注于健全的非诊断实现的程序员在输出像“uint8_t”值这样的内容时不需要担心添加强制转换,但那些可能会被愚蠢的实现所使用的代码的程序员可能希望添加这样的强制转换,以防止编译器对这些实现可能施加的“优化”。


1
这个回答读起来好像是没有考虑到已经在该主题上写过/讨论过的任何内容而写的。我不是那个给你点踩的人,但我也不惊讶有人会这么做。对于用C编写的普通可变参数函数(与标准中指定的抽象函数相反),当向期望无符号参数的函数传递一个带符号1时,行为是明确定义的。问题非常具体,涉及到printf和其它类似函数,这些函数并没有使用va_arg进行规定。 - R.. GitHub STOP HELPING ICE
@R..:也许我应该调整我的答案,使其更加printf-centric,但主要观点是标准的作者们没有理由期望实现会对正有符号值做任何事情,除了将它们视为相应无符号值的同等,因此他们没有理由明确规定printf的行为。如果C标准的作者们想要避免破坏现有代码(这是他们的说法),他们本意是希望将未定义行为视为实现者“行使合理判断”的邀请... - supercat
根据各种因素,关于如何处理某些事情的问题。唯一真正的问题是是否应该依赖所有实现都是合理判断的产物。 - supercat

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