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

16

注意: 我完全重新修订了问题,以更准确地反映我设置奖励的目的。请原谅这可能引起的任何与已有答案不一致的地方。我不想创建一个新的问题,因为先前对此的回答可能是有帮助的。


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

标准定义了scanf函数系列接受的数字格式(%d、%i、%u、%o、%x) 与 strtolstrtoulstrtod 的定义相关联。

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

我试图找到一些能够显示这种差异的值。结果发现,十六进制前缀“0x”,后跟一个不是十六进制数位的字符,就是两个函数族之间差异的一种情况。

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

我想要知道的是解析“0xz”的标准兼容行为是什么?理想情况下引用标准中的相关部分以支持观点。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

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
*/

请注意,当主题字符串生成超过适当类型的值时,strto*函数具有定义行为。然而,对于scanf(),在接收到太大的值时行为是未定义的。因此,将12345678901234567890输入到strtol()将产生错误指示(假设sizeof(long) <= 8),但是使用scanf()等可能会发生任何事情。 - Jonathan Leffler
8个回答

10

与PL22.11(ANSI“C”)的副主席Fred J. Tydeman沟通,有关comp.std.c的内容:

fscanf

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

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

输入项后面的第一个字符(如果有)保持未读状态。(7.19.6.2 P9)

这使得fscanf读取了“z”,并将其作为不匹配的字符回退(遵循脚注251中的单字符回退限制)。

如果输入项不是匹配序列,则指令执行失败:这种情况是匹配失败。(7.19.6.2 P10)

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

strtol

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

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

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


2

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


我同意;scanf()strto*() 函数族必须产生相同的结果;问题在于,虽然 sscanf() 实际上可以使用 strto*(),但由于你提到的原因,fsancf() 却不能。 - Christoph
@DevSolar:标准规定 scanf() 接受与 strto*() 相同的格式,因此如果它们不一致,则是一个 bug。 - Christoph
经过一些思考,我同意了。 - DevSolar
经过更深入的思考、测试和讨论之后,很明显这两个函数族的结果在某些情况下确实不同... :-\ - DevSolar

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

匹配一个可选的带符号十六进制整数,其格式与strtoul函数的主题序列期望值相同,该函数使用base参数的值16。

因此,如果sscanf()strtoul()给出不同的结果,则libc实现不符合规范。
然而,您的示例代码应该得到的预期结果有点不清楚:
如果base16,则strtoul()接受可选的前缀0x0X,规范如下:
主题序列被定义为输入字符串的最长初始子序列,从第一个非空格字符开始,符合预期格式。对于字符串“0xz”,我认为预期形式的最长初始子序列是“0”,因此值应该是0,并且endptr参数应设置为“x”。mingw-gcc 4.4.0不同意这种解释,无法使用strtoul()和sscanf()解析字符串。推理可能是预期形式的最长初始子序列是“0x”-这不是一个有效的整数字面量,因此不进行解析。我认为这种标准的解释是错误的:预期形式的子序列应始终产生有效的整数值(如果超出范围,则返回MIN/MAX值并将errno设置为ERANGE)。
cygwin-gcc 3.4.4(据我所知使用newlib)也不会解析字面量,如果使用strtoul(),但根据我对标准的理解,它将使用sscanf()解析字符串。请注意,我的标准解释容易出现您最初的问题,即标准仅保证能够ungetc()一次。要确定0x是否是文字的一部分,您必须向前读取两个字符:x和后面的字符。如果不是十六进制字符,则必须将它们推回。如果有更多的标记需要解析,您可以缓冲它们并解决此问题,但如果是最后一个标记,则必须ungetc()这两个字符。我不确定fscanf()ungetc()失败时应该做什么。也许只需设置流的错误指示器?

1
@DevSolar:了解一下Sun编译器的功能会很有趣,因为它声称是完全兼容的:http://developers.sun.com/sunstudio/documentation/ss12u1/mr/READMEs/c.html#about - Christoph

2

总结一下,根据标准解析数字时应该发生的情况:

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

    输入字符的最长序列[... ]是匹配的输入序列或其前缀

    根据fscanf()的定义不是

    预期格式的最长初始子序列[...]

    根据strto*()的定义

这有点丑陋,但这是fscanf()应该贪婪的要求所必须的后果,但不能推回多于一个字符。

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

  • strto*()失败以使结果一致是愚蠢的(坏mingw
  • 推回多于一个字符,使fscanf()接受所有被strto*()接受的值违反了标准,但是是合理的(如果他们没有搞砸strto*(),那么新lib万岁
  • 不推回不匹配的字符,但仍然只解析“预期形式”的字符似乎有些可疑,因为字符消失得无影无踪(坏glibc

fscanf()推回超过一个字符并不违反标准 - “一个字符”的限制适用于用户代码,而不适用于标准库本身的实现。 - caf
1
@caf: 标注251)明确指出:“fscanf 最多将一个输入字符推回到输入流中。因此,一些对于 strtodstrtol 等函数可接受的序列对于 fscanf 来说是不可接受的”。 - Christoph
@caf 还要注意的是,标准函数推回的字符和 ungetc() 推回的字符是不同的字符。你的库实现需要允许先前的读取尝试推回一个字符,并且用户在下一次读取或位置查询时推回一个字符。此外,实现可以支持多个字符的用户推回,但 fscanf() 只能推回那一个字符 - 否则你的实现将不符合规范。 - DevSolar

0

问题重写后,答案已过时。 不过评论中有一些有趣的链接。


如有疑问,请编写测试。--谚语
在测试我能想到的所有转换说明符和输入变化的组合后,我可以说,在两个函数族中,它们的结果并不相同。(至少在我可用于测试的glibc中是这样。)
差异出现在三种情况下:
1.您使用“%i”或“%x”(允许十六进制输入)。 2.输入包含(可选的)“0x”十六进制前缀。 3.十六进制前缀后没有有效的十六进制数字。
示例代码:
#include <stdio.h>
#include <stdlib.h>

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"为前缀的十六进制数。因此,它已经读取了'z'并发现它不匹配。但它决定仅使用前导的"0"作为值。这意味着将'z''x'推回。(是的,我知道我在这里使用sscanf()进行简单测试,它不是在流上操作,但我强烈认为他们使所有的...scanf()函数行为一致以保持一致性。)

所以...一个字符的ungetc()在这里真的不是原因吗... ?:-/

是的,结果确实有所不同。尽管如此,我仍然不能适当地解释它...:-(


scanf()函数的返回代码为2,在两种情况下都是如此(匹配%x和%c,%n不计入返回代码,因为这是规范要求的)。 - DevSolar
AIX / xlC还有另一个结果可以提供:值:-1 - 已使用:-1 - 下一个字符:� - (sscanf()) / 值:0 - 已使用:1 - 下一个字符:x - (strtoul()) - DevSolar
2
如果没有人同意,你可以自由地做出你认为正确的决定;在我看来,两个函数的结果应该是Value: 0 - Consumed: 1 - Next char: x;这意味着fscanf()必须向前查看两个字符;如果你不能取消第二个字符,你应该设置流的错误指示器;我认为你不应该像gentoo-gcc 4.1.2那样悄悄地消耗掉x - Christoph
1
另一条可能相关的信息:http://sources.redhat.com/bugzilla/show_bug.cgi?id=1765 - AProgrammer
1
我同意Christoph的观点 - “表示整数的字母和数字序列”必须是非空的,否则它就不代表一个整数。因此,在“0xz”情况下,可选的“0x”不存在,数字序列只是“0”,因此“最终字符串”为“xz”。 - caf
显示剩余7条评论

0

我不确定我理解这个问题,但是scanf()应该处理EOF。scanf()和strtol()是不同类型的函数。也许你应该比较一下strtol()和sscanf()呢?


0

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

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

如果输入是“100”,输出将是“100,9”。我不明白scanf()和ungetc()如何相互干扰。如果我添加了一个幼稚的评论,请原谅。

不是很傻瓜式的。很少有人努力实施标准库函数。;-) 但是ungetc()的操作比仅仅在缓冲区中回退要复杂一些。一,您可能已经到达缓冲区的末尾,并读取了新的缓冲区内容-旧内容不再存在。二,您的流可能根本没有缓冲(考虑setvbuf()和_IONBF)。 (尽管在我的库中,即使对于_IONBF流,我也保留缓冲区,因为这样可以整体上使事情更容易。) - DevSolar

0

对于 scanf() 函数和 strtol() 函数的输入,在 Sec. 7.20.1.4 P7 中指出:如果主题序列为空或不符合预期格式,则不执行转换;只要 endptr 不是空指针,就将 nptr 的值存储在所指向的对象中。此外,您还必须考虑在 Sec. 6.4.4 Constants 规则下定义的解析这些标记的规则,该规则在 Sec. 7.20.1.4 P5 中指出。

其他行为(例如 errno 值)应该是实现特定的。例如,在我的 FreeBSD 系统中,我得到了 EINVALERANGE 值,在 Linux 下发生了同样的情况,其中标准只涉及 ERANGE errno 值。


关于无效规范的部分不适用 - %x 一个有效的转换规范。虽然 strtol() 可以将 endptr 设置为 nptr,但是根据脚注 251,fscanf() 不能这样做... - DevSolar
我知道,但我只想建立更完整的参考,并且我确定它是一个有效的规范。 - daniel

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