代码之家  ›  专栏  ›  技术社区  ›  DevSolar

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

  •  12
  • DevSolar  · 技术社区  · 16 年前

    注: 我彻底改写了这个问题,以便更恰当地反映我为什么设定了奖金。请原谅任何与已经给出的答案不一致的地方。我不想创建一个新问题,因为以前对此问题的回答可能会有所帮助。


    我正在致力于实现C标准库,并对标准的一个特定方面感到困惑。

    该标准定义了 scanf 函数族(%d,%i,%u,%o,%x)的定义如下: strtol , strtoul strtod .

    标准还规定 fscanf() 最多只能将一个字符放回输入流,因此某些序列被 斯特罗尔 , 斯特图尔 字符串转换为浮点数 不可接受的 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
    */
    
    8 回复  |  直到 7 年前
        1
  •  6
  •   DevSolar    16 年前

    与PL22.11(ANSI“C”)副总裁Fred J.Tydeman在comp.std.c上的沟通说明了这一点:

    fscanf

    输入项定义为 输入字符的最长序列 […]是或是前缀 匹配输入序列。(7.19.6.2第9页)

    这使“0x”成为匹配输入序列的前缀的最长序列。(甚至) %i 转换,因为十六进制“0x”的序列比十进制“0”长。)

    第一个字符,如果有的话,在 输入项未读。(7.19.62 p9)

    这使得 FScanf公司 阅读“Z”,并将其放回不匹配位置(遵守脚注251中的一个字符后推限制)。

    如果输入项不匹配 顺序,执行 指令失败:此条件是 匹配失败。(7.19.6.2第10页)

    这使得“0x”无法匹配,即 FSCANF 不应赋值,返回零(如果 %x %i 是第一个conv.specifier),并将“z”保留为输入流中的第一个未读字符。

    strtol

    定义 斯特罗尔 (和) strtoul )一个关键点不同:

    主题序列定义为 最长的初始子序列 输入字符串,从第一个开始 非空白字符, 那是 预期的形式 .(7.20.1.4 P4,重点矿山)

    也就是说 斯特罗尔 应该找最长的 有效的 顺序,在本例中为“0”。应该点 endptr 到“x”,结果返回零。

        2
  •  3
  •   caf    16 年前

    我不认为解析可以产生不同的结果。方格图案的参考只是指出 strtol() 实现可能是另一个更高效的版本,因为它可以完全访问整个字符串。

        3
  •  3
  •   Community Mohan Dere    9 年前

    根据C99规范, scanf() 函数族解析整数的方式与 strto*() 功能系列。例如,对于转换说明符 x 这说明:

    匹配可选签名 十六进制整数,格式为 与预期的主题相同 序列 strtoul 功能 的值16 base 争论。

    所以如果 sscanf() strtoul() 给出不同的结果,libc实现不一致。

    你的预期结果是什么? sample code 不过,应该有点不清楚:

    斯特劳() 接受的可选前缀为 0x 0X 如果 基础 16 ,规格为

    主题序列定义为 最长的初始子序列 输入字符串,从第一个开始 非空白字符,即 所需的窗体。

    对于字符串 "0xz" ,在我看来,预期形式的最长初始子序列是 "0" ,因此该值应为 0 以及 endptr 参数应设置为 X .

    mingwgcccc4.4.0不同意,并且无法同时解析字符串 斯特劳() sSCAN() . 原因可能是预期形式的最长初始子序列是 "0x" -它不是有效的整数文本,因此不进行解析。

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

    cygwin gcc 3.4.4(据我所知,它使用newlib)也不会解析文本if strtoul()。 使用,但根据我对该标准的解释使用 sSCAN() .

    注意,我对标准的解释容易引起你的初始问题,即标准只保证能够 ungetc() 曾经。决定是否 0X 是文字的一部分,您必须提前阅读两个字符: X 以及以下特征。如果不是十六进制字符,则必须将其向后推。如果有更多的令牌要解析,可以缓冲它们并解决这个问题,但是如果它是最后一个令牌,则必须 UNGET() 两个字符。

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

        4
  •  1
  •   Christoph    16 年前

    要总结分析数字时应根据标准执行的操作:

    • 如果 fscanf() 成功,结果必须与通过 strto*()
    • 与…对比 STRTO *() , fSCAN() 如果失败

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

      根据定义 fSCAN() 不是

      预期形式的最长初始子序列[…]

      根据定义 STRTO *()

    这有点难看,但这是要求 fSCAN() 应该是贪婪的,但不能推回多个角色。

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

    • STRTO *() 未能使结果一致是愚蠢的。( 坏明 )
    • 向后推多个字符,因此 fSCAN() 接受接受的所有值 STRTO *() 违反标准,但合理( 如果他们没搞错的话,那就给新自由党欢呼吧。 斯特托*() :( )
    • 当字符消失在稀薄的空气中时,不推回不匹配的字符,但仍然只分析“预期形式”的字符,这似乎有些可疑。( 坏胶质细胞 )
        5
  •  0
  •   Jakob Eriksson    16 年前

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

        6
  •  0
  •   user172818    16 年前

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

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

    如果输入为“100”,则输出为“100,9”。我不知道scanf()和ungetc()会如何相互干扰。对不起,如果我加了一个幼稚的评论。

        7
  •  0
  •   daniel    16 年前

    输入到 SCAN() 功能以及 斯特罗() 函数,在 秒。7.20.1.4P7 表示: 如果主题序列为空或没有预期的格式,则不执行转换;如果endptr不是空指针,则nptr的值存储在endptr指向的对象中。 .另外,您必须考虑解析那些根据规则定义的令牌的规则 第6.4.4常数 ,指向的规则 秒。7.20.1.4P5 .

    其他行为,如 埃尔诺 值,应该是特定于实现的。例如,在我的freebsd盒子里 埃瓦纳尔 厄兰格 值和在Linux下相同,其中标准引用的 厄兰格 ErNO值。

        8
  •  0
  •   DevSolar    7 年前

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


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

    在测试了转换说明符和输入变量的所有组合之后,我可以说这两个函数族是正确的 不要给出相同的结果 . (至少在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” 回来。(是的,我知道 苏桑夫(S) 这是我在这里为了方便测试而使用的,不在流上操作,但我强烈认为它们可以 ...scanf() 函数的行为一致。)

    所以…一炭 ungetc() 不是真正的原因,这里…??

    对, 结果不同 . 尽管如此,我还是无法正确地解释…: