可以`unsigned int x; scanf("%u",&x);` 真的会导致未定义的行为吗?

pip*_*ipe 5 c scanf integer-overflow language-lawyer

有一次我认为我发现了sscanf() 的一个很好的用途,但在阅读了它如何处理整数之后,它似乎不是。有一个应该看起来像这样的字符串:123,456,678我想我可以使用以下代码安全简洁地解析它:

unsigned int x[3];
if( sscanf( s, "%u,%u,%u", x+0, x+1, x+2 ) == 3 )
    …
Run Code Online (Sandbox Code Playgroud)

如果转换失败,我对知道原因并不真正感兴趣,也不担心获得不正确的数据。如果那里有数字以外的东西,scanf()肯定应该创建一个匹配错误并中止,并且它知道我正在寻找一个无符号整数,所以任何负数也应该是一个匹配错误?不。

当我读到转换说明符%u时,我开始怀疑:匹配一个可选的有符号十进制整数。为什么这不是匹配错误?如果签了会怎样?

引自 ISO/IEC 9899:201x 7.21.6.2 ¶ 10,fscanf 函数(重点是我的):

除了 % 说明符的情况外,输入项(或者,在 %n 指令的情况下,输入字符的计数)被转换为适合转换说明符的类型。如果输入项不是匹配序列,则指令执行失败:这种情况是匹配失败。除非赋值抑制由 * 指示,否则转换结果将放置在尚未收到转换结果的格式参数之后的第一个参数所指向的对象中。如果此对象没有合适的类型,或者转换的结果无法在对象中表示,则行为是 undefined

它看起来好像scanf()将每个看起来像整数的转换说明符都视为相同,将输入读取为某种未指定大小的有符号整数,然后绕过所有正常转换写入输出。

例如,根据正常的隐式转换,将任何整数(负数或正数)转换为较小大小的无符号整数表现良好,但不适用于scanf()

unsigned int x;
x = -1;                   /* Well defined: (-1) + (UINT_MAX+1) = UINT_MAX */
sscanf( "-1", "%u", &x ); /* Undefined behavior? */
Run Code Online (Sandbox Code Playgroud)

请告诉我我错了,我错过了标准的某些部分。我无法真正找到参考的一件事是上面引用的部分的这一部分:“输入项 (...) 被转换为适合转换说明符的类型”。如果转换说明符是%u,那么任何负数当然不合适,任何不适合无符号整数的东西也不合适。但是,我在标准中找不到任何东西告诉我什么是“适当的类型”。

我发现了一些直接或间接处理这个问题的问题,但不是很详细。与我最相似的问题是C:如何防止使用 scanf 的输入溢出?但它的框架并不那么具体。一些答案 ( 1 , 2 ) 提到了这个问题,但没有提供细节或参考。

我的问题的目标是得到一个答案,详细说明为什么除了未定义的行为之外不能以任何方式解释这一点,最好有一些理由说明为什么这是有道理的 - 完全知道 C 中的某些事情是不一致的,我有接受它。

aut*_*tic 1

有一次我以为我找到了 sscanf() 的一个很好的用途,但是在阅读了它如何处理整数之后,它似乎没有

至于忽略 C 中的工具的建议,因为它可能很危险,我经常将其视为对抗甚至传统 C 字符串的武器scanf......goto但最终,整个语言呈现出微妙的危险,所以你'最好遵循(正确地)使用正确的工具来完成工作的建议,而 C 对于大多数工作来说大多不是正确的工具!请记住这一点;有时,货物崇拜思维会让您忽视最好的工具。另外,关于正确性,我确信您知道您应该考虑大多数标准库函数的返回值,正是由于这些常见的遗漏,才产生了这种货物崇拜思想。要正确使用一个工具,我们必须阅读它的手册,fscanf手册中非常清楚地说明了返回值的重要性。看到人们阅读这样的手册真是令人耳目一新(感谢提出这样的问题)

就你的问题而言,我已经列出了我认为你想要答案的那些问题,并将再次讨论这些问题。然而,首先,您似乎发现了一些不准确的前提。例如,您可能掩盖了第 7.21.6.2 节第 9 段(实际上是您引用的 p10 之前的段落)中的一些必要细节,因此很难说您对术语“输入项”的理解是否正确:

输入项被定义为最长的输入字符序列,该序列不超过任何指定的字段宽度,并且是匹配输入序列或者是匹配输入序列的前缀。

所以事实上你后来的问题是:

如果签署了会发生什么?

...本质上与以下相同:

当(字符序列)输入项以“-”字符开头时会发生什么?

我不能确定会发生什么,因为您的实现有许多选项可供遵守,而且它似乎取决于标准库。标准中有几个地方回避了优化,例如第 5.1.2.3p4 和 p6 节中的“as if”规则。将实现细节留给实现的原因是为了让实现有机会进行优化,否则这是不可能的。可以说,转变将会发生。在这个答案中,我将给出一种标准库可以满足此要求(转换)的方法,但请放心,这只是一种可能性,还有许多其他可能性,您的编译器甚至可能会将此代码替换为更优化的代码(不同的转换)。

其他部分中有描述有符号到无符号转换的详细信息,例如第6.3.1.3p2节,没有未定义的行为

否则,如果新类型是无符号的,则通过重复加或减比新类型可以表示的最大值大一的方式来转换该值,直到该值在新类型的范围内。

可以预期,当输入以负号开头时,scanf相关函数要么沿着该逻辑线执行显式转换,要么使用其中一个运算符(如提供该转换的6.3中所述。例如,在您的标准中)库可能看起来像这样:

int c = fgetc(file);
unsigned u = 0;
switch (c) {
    case '-':
    { int d = 0;
      while (isnum(c = fgetc(file)))
      { d *= 10;
        d -= (c - '0');
      }
      if (c >= 0) ungetc(c, file);
      u = d; // here's your signed-to-unsigned conversion, with no UB
      break;
    }
    default:
      while (isnum(c))
      { u *= 10;
        u += c;
        c = fgetc(file);
      }
      if (c >= 0) ungetc(c, file);
}
Run Code Online (Sandbox Code Playgroud)

现在,既然我们已经展示了标准库如何在这种情况下符合要求,那么现在就开始讨论另一个问题(您的第一个问题):

为什么这不是匹配错误?

如果你有敏锐的眼光,你可能会发现我的代码可能不那么两面派。如果我不得不冒险猜测,他们想要模块化代码以减少 L1 缓存抖动(因为这曾经是一个比现在更严重的问题),他们设计了巧妙的模式来用相同的逻辑匹配各种数字数据。你可以问同样的关于 pp-number 元素的问题答案一样的:如果 C 没有“好像”规则,那么它们在实践中可能会慢得多......