解析数字时scanf()和strtol()/ strtod()之间的区别

注意:我完全重写了这个问题,以更恰当地反映出我为此设置的赏金。 请原谅与已经给出的答案有任何不一致之处。 我不想创建一个新问题,因为之前的答案可能会有所帮助。


我正在努力实现C标准库,并对标准的一个特定角落感到困惑。

该标准根据strtolstrtoulstrtod的定义定义了scanf函数系列接受的数字格式(%d,%i,%u,%o,%x)。

该标准还说fscanf()只会将最多一个字符放回输入流中,因此strtolstrtoulstrtod接受的某些序列对fscanf是不可接受的(ISO / IEC 9899:1999,脚注251)。

我试图找到一些会表现出这种差异的价值观。 事实certificate,hex前缀“0x”,后跟一个不是hex数字的字符,就是两个函数系列不同的情况。

有趣的是,很明显没有两个可用的C库似乎在输出上达成一致。 (请参阅本问题末尾的测试程序和示例输出。)

我想听到的是解析“0xz”时会被视为符合标准的行为? 。 理想情况下引用标准中的相关部分来说明问题。

 #include  #include  #include  int main() { int i, count, rc; unsigned u; char * endptr = NULL; char culprit[] = "0xz"; /* File I/O to assert fscanf == sscanf */ FILE * fh = fopen( "testfile", "w+" ); fprintf( fh, "%s", culprit ); rewind( fh ); /* fscanf base 16 */ u = -1; count = -1; rc = fscanf( fh, "%x%n", &u, &count ); printf( "fscanf: Returned %d, result %2d, consumed %d\n", rc, u, count ); rewind( fh ); /* strtoul base 16 */ u = strtoul( culprit, &endptr, 16 ); printf( "strtoul: result %2d, consumed %d\n", u, endptr - culprit ); puts( "" ); /* fscanf base 0 */ i = -1; count = -1; rc = fscanf( fh, "%i%n", &i, &count ); printf( "fscanf: Returned %d, result %2d, consumed %d\n", rc, i, count ); rewind( fh ); /* strtol base 0 */ i = strtol( culprit, &endptr, 0 ); printf( "strtoul: result %2d, consumed %d\n", i, endptr - culprit ); fclose( fh ); return 0; } /* newlib 1.14 fscanf: Returned 1, result 0, consumed 1 strtoul: result 0, consumed 0 fscanf: Returned 1, result 0, consumed 1 strtoul: result 0, consumed 0 */ /* glibc-2.8 fscanf: Returned 1, result 0, consumed 2 strtoul: result 0, consumed 1 fscanf: Returned 1, result 0, consumed 2 strtoul: result 0, consumed 1 */ /* Microsoft MSVC fscanf: Returned 0, result -1, consumed -1 strtoul: result 0, consumed 0 fscanf: Returned 0, result 0, consumed -1 strtoul: result 0, consumed 0 */ /* IBM AIX fscanf: Returned 0, result -1, consumed -1 strtoul: result 0, consumed 1 fscanf: Returned 0, result 0, consumed -1 strtoul: result 0, consumed 1 */ 

与comp.std.c上的PL22.11(ANSI“C”)副主席Fred J. Tydeman的沟通揭示了这一点:

fscanf

输入项被定义为输入字符的最长序列,它是匹配输入序列的前缀,或者是匹配输入序列的前缀。 (7.19.6.2 P9)

这使得“0x”成为匹配输入序列的前缀的最长序列。 (即使使用%i转换,因为hex“0x”是比十进制“0”更长的序列。)

输入项目之后的第一个字符(如果有)仍未读取。 (7.19.6.2 P9)

这使fscanf读取“z”,并将其作为不匹配(遵守脚注251的单字符后推限制))。

如果输入项不是匹配序列,则指令的执行失败:此条件是匹配失败。 (7.19.6.2 P10)

这使得“0x”无法匹配,即fscanf不应分配任何值,返回零(如果%x%i是第一个转换说明符),并将“z”保留为输入流中的第一个未读字符。

strtol

strtol (和strtoul )的定义在一个关键点上有所不同:

主题序列被定义为输入字符串的最长初始子序列,从第一个非空白字符开始, 即预期forms 。 (7.20.1.4 P4,强调我的)

这意味着strtol应该寻找最长的有效序列,在本例中为“0”。 它应该将endptr指向“x”,并返回零作为结果。

我不相信解析可以产生不同的结果。 Plaugher引用只是指出strtol()实现可能是一个不同的,更高效的版本,因为它可以完全访问整个字符串。

根据C99规范, scanf()系列函数以与strto*()函数族相同的方式解析整数。 例如,对于转换说明符x它读取:

匹配可选的带符号hex整数,其格式与strtoul函数的主题序列的预期相同,其base值参数值为16。

因此,如果sscanf()strtoul()给出不同的结果,则libc实现不符合。

您的示例代码的预期结果应该是什么有点不清楚,但是:

如果base16 ,则strtoul()接受可选前缀0x0X ,并且规范读取

主题序列被定义为输入字符串的最长初始子序列,从第一个非空白字符开始,即预期forms。

对于字符串"0xz" ,在我看来,期望forms的最长初始子序列是"0" ,因此该值应为0并且endptr参数应设置为x

mingw-gcc 4.4.0不同意并且无法使用strtoul()sscanf()解析字符串。 推理可能是预期forms的最长初始子序列是"0x" – 这不是有效的整数文字,因此不进行解析。

我认为对标准的这种解释是错误的:预期forms的子序列应该总是产生一个有效的整数值(如果超出范围,则返回MIN / MAX值并将errno设置为ERANGE )。

cygwin-gcc 3.4.4(据我所知使用newlib)如果使用了strtoul() ,也不会解析文字,但是根据我对sscanf()的标准解释来解析字符串。

请注意,我对标准的解释倾向于你的初始问题,即标准只保证能够ungetc()一次。 要确定0x是否是文字的一部分,您必须提前读取两个字符: x和后面的字符。 如果它不是hex字符,则必须将其推回。 如果有更多的标记要解析,你可以缓冲它们并解决这个问题,但如果它是最后一个标记,你必须ungetc()这两个字符。

如果ungetc()失败,我不确定fscanf()应该做什么。 也许只是设置流的错误指示器?

总结解析数字时根据标准应该发生的事情:

  • 如果fscanf()成功,结果必须与通过strto*()获得的结果相同
  • strto*()fscanf()失败了

    输入字符的最长序列,它是匹配输入序列的前缀,或者是匹配输入序列的前缀

    根据fscanf()的定义不是

    最长的初始子序列[…]具有预期的forms

    根据strto*()的定义

这有点难看,但是要求fscanf()应该是贪婪但不能推回多个字符的必然结果。

一些库实现者选择了不同的行为。 在我看来

  • strto*()无法使结果保持一致是愚蠢的( 糟糕的mingw
  • 推回多个字符,因此fscanf()接受strto*() fscanf()接受的所有值都违反了标准,但是有理由( 如果他们没有使用strto*()话,请为newlib strto*() 🙁
  • 没有推回不匹配的角色,但仍然只解析“预期forms”的角色似乎是可疑的,因为角色消失在空气中( 坏glibc

我不确定我是否理解这个问题,但有一件事,scanf()应该处理EOF。 scanf()和strtol()是不同种类的野兽。 也许你应该比较strtol()和sscanf()?

我不确定如何实现scanf()可能与ungetc()有关。 scanf()可以用完流缓冲区中的所有字节。 ungetc()只是将一个字节推送到缓冲区的末尾,并且偏移量也会改变。

 scanf("%d", &x); ungetc('9', stdin); scanf("%d", &y); printf("%d, %d\n", x, y); 

如果输入为“100”,则输出为“100,9”。 我没有看到scanf()和ungetc()如何相互干扰。 对不起,如果我添加了一个天真的评论。

对于scanf()函数和strtol()函数的输入,在Sec。 7.20.1.4 P7表示: 如果主题序列为空或没有预期的forms,则不进行转换; 如果endptr不是空指针,则nptr的值存储在endptr指向的对象中 。 此外,您必须考虑解析那些根据Sec规则定义的令牌的规则 6.4.4常量第二节中指出的规则 7.20.1.4 P5

其余行为(例如errno值)应该是特定于实现的。 例如,在我的FreeBSD盒子里,我得到了EINVALERANGE值,在Linux下也是如此,标准引用只有ERANGE errno值。

在重写问题后回答过时。 虽然评论中有一些有趣的链接。


如有疑问,请写一个测试。 – 谚语

在测试了我能想到的转换说明符和输入变量的所有组合之后,我可以说这两个函数族没有给出相同的结果是正确的。 (至少在glibc中,这是我可用于测试的。)

当三种情况相遇时会出现差异:

  1. 您使用"%i""%x" (允许hex输入)。
  2. 输入包含(可选) "0x"hex前缀。
  3. hex前缀后面没有有效的hex数字。

示例代码:

 #include  #include  int main() { char * string = "0xz"; unsigned u; int count; char c; char * endptr; sscanf( string, "%x%n%c", &i, &count, &c ); printf( "Value: %d - Consumed: %d - Next char: %c - (sscanf())\n", u, count, c ); i = strtoul( string, &endptr, 16 ); printf( "Value: %d - Consumed: %td - Next char: %c - (strtoul())\n", u, ( endptr - string ), *endptr ); return 0; } 

输出:

 Value: 0 - Consumed: 1 - Next char: x - (sscanf()) Value: 0 - Consumed: 0 - Next char: 0 - (strtoul()) 

这让我很困惑。 显然sscanf()不会在'x'处挽救,或者它无法解析任何 "0x"前缀hex。 所以它读了'z'并发现它不匹配。 但它决定只使用前导"0"作为值。 这意味着将'z' 'x'推回去。 (是的,我知道我在这里用于简单测试的sscanf()不能在流上运行,但我强烈认为它们使得所有...scanf()函数的行为完全一致。)

所以… one-char ungetc()并不是真正的原因,这里……?: – /

是的, 结果不同 。 我仍然无法正确解释,但…… 🙁