`printf("%.-1s\n", "foo")` 是否会导致未定义行为?

7

根据标准

每个转换说明符都以字符%开头。在%之后,按照以下顺序依次出现:

  • 零个或多个标志[...]。
  • 可选的最小字段宽度。[...]
  • 一个可选的精度,该精度 [...] 给s转换提供要写入的最大字节数。精度采用句点(.)后面跟一个可选的十进制整数的形式;
  • 可选的长度修改符[...]。+一个转换说明符字符[...]。
  • 可选的最小字段宽度。[...]
  • 一个转换说明符字符[...]。

后来:

负精度参数被视为省略精度。

根据我对标准定义的理解,我期望从 printf("%.-1s\n", "foo") 输出的结果是:

我从标准中引用的第二段话表明,我们可以传递负精度参数,并且会忽略这样的精度。

所以,printf("%.-1s\n", "foo") 应该等同于 printf("%s\n", "foo"),它将显示 "foo\n" 并返回4

然而,在我使用的系统上(osx)printf("%.-1s\n", "foo") 的实际表现是:

printf("%.-1s\n", "foo")显示" \n"并返回2

这显然与我的期望不同。

  • 我对标准的解释有误吗?
  • 这种行为是未定义的吗?
  • 是否可以传递负的精度(编辑:没有星号)?

2
我认为关于负数参数的部分只是在讨论当你使用 * 从参数中获取值时的情况,而不是将精度放在格式字符串中。 - Barmar
2
你省略了7.21.6.1p5的相关部分:“如上所述,星号可以表示字段宽度、精度或两者兼备。在这种情况下,int参数提供字段宽度或精度。...负字段宽度参数被视为带有正字段宽度的-标志。负精度参数被视为省略了精度。”因此,关于负值的部分明显与传递的参数有关,而不是字符串本身中的十进制数。阅读标准时,所有单词都很重要! - too honest for this site
1
@vmonteco:再说一遍:“十进制整数”不包括符号!这甚至没有意义,这就是为什么明确提到参数字段宽度无效(因为负值不能使其无效)的原因。 - too honest for this site
3
标准在多个地方提到“十进制整数”、“非负十进制整数”和“有符号十进制整数”。因此看起来可以存在“负的”和“有符号的”整数。 - Eugene Sh.
1
@Olaf,我确实读了这条评论,但是恕我直言,这并不明显,因为正如Eugene所说,有些整数字段被定义为“非负数”。 - vmonteco
显示剩余23条评论
3个回答

4

N1570-§7.21.6.1/p5:

正如上面所述,星号可以指示字段宽度、精度或两者兼而有之。在这种情况下,一个int参数提供字段宽度或精度。指定字段宽度、精度或两者兼而有之的参数应该(按照那个顺序)出现在要转换的参数(如果有)之前。负字段宽度参数被视为一个-标志,后跟一个正字段宽度。负精度参数被视为省略了精度。

标准规定,这仅适用于在格式字符串中使用星号作为精度,并且将负值作为参数传递,如下所示:

printf("%.*s\n", -1, "foo");  // -1 will be ignored  

在第四段中,它说:精度的形式为句点( . ),后面跟一个星号 * (稍后描述)或一个可选的小数整数;但它并没有明确说明小数整数是否应大于0(就像7.21.6.2/p3部分的scanf字段宽度一样)。在这一点上,标准似乎不明确,结果可能与机器有关。

@EugeneSh。目前我同意了,但事实上标准明确指定,在格式字符串中使用“.”后跟“*”时,负精度是一个参数。 - haccks
即使标准仍然对此存在歧义,您给出的答案和解释似乎被一些测试 事实上 确认:负精度字段似乎不被视为有效,并且因此似乎会引发未定义行为。 - vmonteco
@EugeneSh。这让我想起来,因为我的头脑不愿意接受这样的事情存在,并将其与实数联系起来。我已经删除了那部分内容。 - haccks
但如果在我实际使用的系统上似乎是这种情况,我猜其他实现可能不是这种情况。 - vmonteco

1
  • 我的标准解释有误吗?

我理解您的解释如下:

因此,printf("%.-1s\n", "foo") 应该等同于 printf("%s\n", "foo"),它将显示 "foo\n" 并返回 4。

不是这样的。您引用的关于忽略负精度参数的规定不适用于此情况。该规定是指在格式字符串中将精度指定为 *,并将值作为单独的 printf 参数传递的选项:

printf("%.*s\n", -1, "foo");

在这种情况下,负精度参数会导致printf()的行为就像没有指定精度一样。您的情况不同。
另一方面,标准并未要求出现在格式字符串中的精度值为非负十进制整数。它在该部分的其他几个地方对术语“十进制整数”进行了限定,但在关于精度字段的段落中没有这样做。
“这种行为是否未定义?”不是。有两种相互冲突的所需语义解释(见下文),但无论哪种方式,标准都定义了行为。它可以被解释为:当直接在格式字符串中呈现负精度值时,也适用于负精度参数的行为描述。这具有一致性的优点,并且是您报告观察到的行为。然而,标准的字面阅读将表明,当将精度作为负十进制整数呈现在格式字符串中时,那么该部分描述的普通语义将适用;对于s指令,这将是负精度表示要输出的最大字符数。
你所观察到的行为与前一种解释不一致,但考虑到输出少于0字节的实际困难,我并不惊讶后一种解释没有成功实现。我倾向于猜测后一种解释是你的实现试图实现的内容。
我怀疑在某个阶段无意中遗漏了为精度字段提供负值的可能性,但无论是否有意,标准似乎允许这样做。

我认为这里没有观察到任何一种解释... https://ideone.com/UTvjoM - Eugene Sh.
我不同意标准允许这样做。同意,不使用“非负数”来表示精度可能被认为是一个缺陷。然而,字段宽度是一个不好的比喻,因为“-”标志会使其看起来像一个负值。允许负的字段宽度(并将其解释为带有“*”参数的“-”标志加上一个非负字段宽度)支持了这一点。从标准的其他地方来看,在墙外看看,6.4.4.2p3(浮点常量)“……指数部分中的数字序列被解释为十进制整数……”显然是关于一个非负的,即只包含数字的值。 - too honest for this site
当在格式字符串中直接提供负精度值时,所描述的负精度参数行为也适用。这具有一致性的优点,并且这是您所观察到的行为。 实际上,这正是我所期望但没有观察到的情况。 - vmonteco
1
@Olaf,这里使用的“十进制整数”一词并不是指整数常量(因为标准在其他地方使用该术语)。如果标准的意思相同,则会使用相同的术语。此外,标准中在有符号性限定词(特别是“非负”)与“十进制整数”术语一起使用时,反驳了当未加资格限制时,该术语应被解释为不允许负数的命题。 - John Bollinger
1
@Olaf - 我真的怀疑标准中的“十进制整数”是否指非负值。有些地方明确说明“十进制整数”必须是“无符号的”或“非负的”。如果“十进制整数”总是非负的,为什么要这样做呢?此外,转换说明符d的描述说“匹配一个可选的带符号十进制整数”。我认为他们可能忘记在一些地方包括“非负”的内容了。 - Support Ukraine
显示剩余7条评论

1
在类似于"%-5d"的格式中,宽度不是-5;相反,减号是一个“标志字符”,表示值应该左对齐在给定宽度的字段中。使用“非负数”来描述宽度与“-”是标志字符而不是符号的事实相关联。虽然标准没有规定精度必须为非负数,但很难想象出任何不刻意的目的会说遇到一个点和一些小数位之间的“-”时,实现必须忽略这些数字的内容。一些实现可能会以这种方式处理事情,但许多实现可能没有任何代码来显式处理格式中该位置上的“-”,而要么将其视为格式开头的“-”,要么将其视为没有定义含义的任何其他字符,具体取决于哪个更方便。我认为没有理由将任何一种行为视为“有缺陷”。

如果在运行时构建格式:sprintf(format, "%%.%ds", some_int),那么无论some_int的值如何,printf(format, "Hello_World")都不会引起未定义行为是很好的。然而,这是一个有点牵强附会的情况,因为可以使用sprintf(format,"%%.%us",some_unsigned) - chux - Reinstate Monica
@chux:许多实现为格式字符串定义了超出标准规定的含义;如果一个实现使用%@d作为特殊格式,它接受一个包含用于数字0-9的字符的char*,但代码传递了其他类型的参数,那么可能会导致各种混乱。标准没有试图区分实现可能合理实现某些有意义的行为的情况,这些行为如果代码不期望它们而出错,与那些应该以更受限制的方式运行的情况。 - supercat

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