【问题标题】:Does `printf("%.-1s\n", "foo")` invoke undefined behaviour?`printf("%.-1s\n", "foo")` 会调用未定义的行为吗?
【发布时间】:2017-11-22 03:54:04
【问题描述】:

根据the standards

每个转换规范都由字符 % 引入。后 %,以下依次出现:

  • 零个或多个标志 [...]。
  • 可选的最小字段宽度。 [...]
  • 一个可选精度,为 [...] 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

这显然与我的预期不同。

  • 难道我对标准的解释有误吗?
  • 此行为是否未定义?
  • 实际上是否可以传递负精度(编辑:不带星号)?

【问题讨论】:

  • 我认为关于否定论点的部分只是在谈论使用* 从参数中获取值的情况,而不是将精度放入格式字符串中。
  • 您省略了 7.21.6.1p5 的相关部分:“如上所述,字段宽度或精度或两者都可以用星号表示。在这种情况下,int 参数提供字段宽度或精度。......负字段宽度参数被视为 - 标志,后跟正字段宽度。负精度参数被视为好像省略了精度。所以关于负值的部分显然与传递的参数有关,而不是字符串本身的小数。阅读标准时,所有字词都很重要!
  • @vmonteco:再说一遍:“十进制整数”不包括符号!这甚至没有意义,这就是为什么明确提到参数字段宽度无效的原因(因为不能使负值无效)。
  • @Olaf 该标准在几个地方提到了“十进制整数”、“非负十进制整数”和“有符号十进制整数”。所以看起来可能有“否定”和“签名”。
  • @Olaf 我确实阅读了这条评论,但恕我直言,这并不明显,因为 - 正如 Eugene 所说 - 一些整数字段被定义为“非负数”。

标签: c printf language-lawyer standards undefined-behavior


【解决方案1】:

N1570-§7.21.6.1/p5:

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

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

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

在第 4 段中说:

[...] 精度采用句点 (.) 的形式,后跟一个 星号*(稍后描述)或可选的十进制整数; [...]

但它没有具体说明十进制整数是否应大于0(如第 7.21.6.2/p3 节中字段宽度为scanf 的情况下所说)。标准在这一点上似乎模棱两可,结果可能取决于机器。

【讨论】:

  • @EugeneSh.;暂时我同意,但事实是标准明确指定 -ve 精度作为参数,以防* 在格式字符串中的. 之后使用。
  • 即使标准在这方面听起来仍然模棱两可,但您的回答和您给出的解释似乎事实上已通过某些测试得到证实:似乎不接受负精度字段是有效的,因此似乎会调用未定义的行为。
  • @EugeneSh.;我想到了这一点,因为我的想法不愿意接受这样的事情甚至存在,我将它与实数联系起来。我删除了那部分。
  • (但如果在我实际使用的系统上似乎是这样,我猜其他实现可能不是这样。)
【解决方案2】:
  • 难道我对标准的解释有误吗?

我把你的解释总结为:

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

没有。您引用的关于负精度 arguments 被忽略的规定不适用于这种情况。该条款讨论了在格式字符串中将精度指定为* 并将值作为单独的printf 参数传递的选项:

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

在这种情况下,负精度参数导致printf() 表现得好像没有指定精度一样。你的情况不同。

另一方面,标准在这里并不要求出现在格式字符串中的精度值是非负十进制整数。它确实在其他几个地方以这种方式限定了术语“十进制整数”,包括同一部分的前面,但在有关精度字段的段落中没有这样做。

  • 此行为是否未定义?

没有。对所需语义有两种相互矛盾的解释(见下文),但无论哪种方式,标准都定义了行为。它可以解释为

  • 当负精度值直接出现在格式字符串中时,为负精度参数描述的行为也适用。这具有一致性的优势,它是您报告观察到的行为。然而,

  • 标准的字面解读表明,当精度在格式字符串中以负十进制整数表示时,则适用该节中描述的普通语义;对于s 指令,负精度表示要输出的最大字符数。

您观察到的行为与前一种解释不一致,但考虑到输出少于 0 字节的实际困难,后一种解释没有成功实现对我来说不足为奇。我倾向于猜测后者是您的实现试图实现的。

我怀疑在某个阶段允许为精度字段提供负值的可能性是无意的遗漏,但无论有意与否,标准似乎都允许这样做。

【讨论】:

  • 我认为这里实际上没有观察到任何解释...ideone.com/UTvjoM
  • 我不同意标准允许这样做。同意,不使用“非负”来表示精度可以被认为是一个缺陷。然而,字段宽度是一个不好的类比,因为- 标志会使它看起来像一个负值。允许负字段宽度(并将其解释为 - 标志加上带有 * 参数的 nmon 负字段宽度支持这一点。查看标准中的其他地方,查看墙壁,6.4.4.2p3 (浮点常量)“……指数部分中的数字序列被解释为十进制整数……”显然是关于非负数,即仅数字值。
  • "当负精度值直接出现在格式字符串中时,为负精度参数描述的行为也适用。这具有一致性的优点,它是您报告观察到的行为.”实际上,这正是我所期待但没有观察到的情况。
  • @Olaf,这里使用的“十进制整数”术语不是指整数常量(因为其他地方的标准使用该术语)。如果标准的含义相同,它将使用相同的术语。此外,在标准的其他地方使用“十进制整数”术语连同其符号性的限定词(尤其是“非负数”)反表明了这样一种命题,即当不合格时,该术语应被解释为不允许负数。
  • @Olaf - 我真的怀疑标准中的“十进制整数”是指非负值。有些地方明确指出“十进制整数”必须是“无符号”或“非负数”。如果“十进制整数”始终为非负数,为什么要这样做?进一步的转换说明符 d 的描述说“匹配可选的 signed 十进制整数”。我宁愿认为他们忘记包含“非负数”a几个地方。
【解决方案3】:

"%-5d" 这样的格式中,宽度不是-5;相反,- 符号是一个“标志字符”,它表示一个值应该在给定宽度的字段中左对齐。宽度使用“非负数”与“-”是标志字符而不是符号这一事实有关。虽然标准没有指定精度必须是非负数,但很难想象任何非人为的目的可以通过说在句点和一些十进制数字之间遇到“-”的实现必须忽略这些数字的内容。某些实现可能会以这种方式处理事物,但很可能许多实现没有任何代码来显式处理该位置的“-”,并且会将其视为与开头的“-”相同格式化或将其视为没有定义含义的任何其他字符,这取决于哪个更方便。我认为没有理由将这两种行为视为“缺陷”。

【讨论】:

  • 如果格式在运行时被构造:sprintf(format, "%%.%ds", some_int);,如果printf(format, "Hello_World");不调用UB就好了,不管some_int。然而,这有点做作,因为可以使用 sprintf(format, "%%.%us", some_unsigned)
  • @chux:许多实现定义了超出标准规定的格式字符串的含义;如果实现使用%@d 作为一种特殊格式,它采用包含用于数字0-9 的字符的char*,但代码传递的不是char*,则可能会导致各种混乱。该标准没有试图区分实现可能合理地实现一些故意有用的行为(如果代码没有预料到它可能会出错)与那些应该以更受约束的方式表现的情况。
猜你喜欢
  • 1970-01-01
  • 2014-01-28
  • 1970-01-01
  • 1970-01-01
  • 2013-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多