这个fscanf的行为是否不一致?

4
通常情况下,使用%d扫描非整数时,fscanf会一直失败,直到将非整数字符从输入流中明确删除为止。尝试扫描a123将失败,直到将a从输入流中删除为止。
尝试扫描------123也会失败(fscanf返回0),但-已经从输入流中删除了。
这是fscanf的正确行为吗?
文件包含----------123,执行此代码的结果为:
#include <stdio.h>

int main(void) {
    int number = 0;
    int result = 0;
    FILE *pf = NULL;

    if (NULL != (pf = fopen("integer.txt", "r"))) {
        while (1) {
            if (1 == (result = fscanf(pf, "%d", &number))) {
                printf("%d\n", number);
            } else {
                if (EOF == result) {
                    break;
                }
                printf("result is %d\n", result);
            }
        }
        fclose(pf);
    }
    return 0;
}

是:

result is 0
result is 0
result is 0
result is 0
result is 0
result is 0
result is 0
result is 0
result is 0
-123

如果文件包含a123,结果将会是一个无限循环。
在我看来,这种行为似乎是不一致的。是吗?

2
“-” 是一个整数的有效首字符。 - stark
1
这是文档记录的行为。 - stark
2
如果你试图编写自己的 scanf 实现,这是一个“不可能”的情况。你已经读取了第一个 -,它可能是负整数的开头。然后你读取了第二个 -,它不是数字,这意味着你的扫描失败了。你可以将第二个 - 推回去,但是 ungetc 只保证一个字符的推回。所以推回第一个 - 可能很困难或不可能。我怀疑这就是为什么你看到它被消耗掉的原因。 - Steve Summit
1
@user3121023 我明白。问题在于fscanf很难做到这一点。当它意识到无法转换时,它已经读取了两个字符,因此需要将它们都返回到输入流中。但是stdio使用的推回机制通常只保证一个字符的推回(请参见man ungetc)。 - Steve Summit
1
你可以使用 ungetc() 放回任何字符,它不一定是之前读取的那个字符。 - Jonathan Leffler
1
@SteveSummit:实际上情况比这更糟:*scanf() 不能使用ungetc()。那一个字节的ungetc()是保留给用户的。必须有一个第二个字节的“内部ungetc()”用于*scanf(),并且库实现不能混淆它们...;-) - DevSolar
3个回答

7
这里的问题不在于不一致,而是在于fscanf()家族的许多限制之一。标准非常明确地说明了fscanf()如何解析输入。字符逐个从输入中取出,并与格式字符串进行匹配。如果匹配成功,则下一个字符将从输入中取出。如果不匹配,则该字符被“放回”,转换失败。但是,只有最后读取的那个字符会被放回。C11 7.21.6.2 fscanf函数第9段(我强调):输入项被定义为不超过任何指定字段宽度且是匹配输入序列的最长输入字符序列,或者是其前缀。285)输入项后的第一个字符(如果有)保持未读状态。fscanf最多向输入流推送一个输入字符。因此,某些可接受strtod、strtol等的序列对于fscanf来说是不可接受的。
这个“推回”字符与ungetc()保证的那个“推回”字符没有任何关系——它是独立的,而且额外的。(用户可能会让fscanf()失败,然后ungetc()一个字符,并期望ungetc()的字符出现在输入中,紧随其后的是由失败的fscanf()推回的字符。*库函数可能不会调用ungetc(),这是保留给用户的。)
这使得实现扫描fscanf()变得更加容易,但也使fscanf()在某些字符序列的中途失败,而不是重新跟踪到它开始转换的位置。
在您的情况下,"--123"读作"%d"
- 取第一个'-'。符号。一切正常,继续。 - 取第二个'-'。匹配错误。 - 放回最后一个'-'。不能像上面那样放回第二个'-'。 - 返回0(转换失败)。
这就是你不应该在可能存在格式问题的输入上使用*scanf()的原因之一:扫描可能失败,而你并不知道它失败的确切位置,也没有正确地回滚。
这也是标准中一个模糊的角落,在许多主流库实现中并没有得到正确的实现 我上次检查时。(我现在重新检查时也不行。);-)
不使用fscanf()的其他原因包括但不限于,对潜在格式错误的输入进行数字溢出处理时不够优雅。 fscanf()的预期用途是扫描已知格式良好的数据,最好是由同一程序使用fprintf()编写的数据。它不适合解析用户输入。
因此,通常建议使用fgets()读取完整的输入行,然后在内存中解析该行,使用strtol()strtod()等函数可以很好地处理上述问题。

“only that last character read is ever put back.” 这句话在规范的“fscanf pushes back at most one input character onto the input stream”脚注中得到了一定的支持。然而,脚注仅供参考,它并没有明确规定第二个、第三个……字符会发生什么。我的GCC将两个“-”都推回去,导致OP的代码进入无限循环。根据我阅读的C17 § 7.21.6.2,允许推回超过1个字符(如ungetc()),或者可能失败——这可能导致UB或实现特定的行为。关于fgets()的说明非常好。 - chux - Reinstate Monica
@chux-ReinstateMonica:请查看更新的答案;包含脚注的完整段落使预期行为变得清晰明了:输入项——最长匹配序列的前缀——是 '-'。输入项后的第一个字符——第二个 '-'——保持未读状态。(通过使用 %i%x 读取 "0xz" 的情况会更加清晰。"0x" 被匹配,'z' 不匹配,“正确的”做法是仅匹配 '0',但由于单字符限制,'x' 无法被放回,因此整个匹配过程必须失败。)GLibC 在这里进行了一些自由处理。 - DevSolar
如果您查看我链接的问答,这不是我的解释,这已经通过与PL22.11(ANSI“C”)的副主席Fred J. Tydeman的对话进行了验证。现有实现未遵守此解释的关键问题在于,首先使用fscanf()就是一种病态滥用,没有好的恢复方式,因此它并不真正重要。 - DevSolar

2

fscanf的行为是否正确?

是的,正如评论中@stark所指出的那样,当您使用%d作为格式说明符时,-是结果的一部分。

如果您想扫描一个正整数(仅数字),则可以在fscanf中使用模式来丢弃所有非数字字符。

fscanf(pf, "%*[^0-9]%d", &number)

fscanf(pf, "%*[^0-9]%d", &number) 无法扫描 "123",因为没有任何内容与 "%*[^0-9]" 匹配。也许可以使用 fscanf(pf, "%*[^0-9]"); fscanf(pf, "%d", &number); - chux - Reinstate Monica

2

这种行为是被规定的:

以下是来自C2x标准的相关段落:

7.21.6.2函数fscanf

[...]

7   指示符作为转换说明符定义了一组匹配的输入序列,对于每个说明符都如下所述。 转换说明符按以下步骤执行:
8   跳过输入中的空白字符,除非说明符包括 [cn 说明符。
9   从流中读取一个输入项,除非说明符包括 n 说明符。 输入项被定义为不超过任何指定字段宽度的最长输入字符序列,该序列是匹配输入序列或其前缀。如果输入项的长度为零,则指令的执行失败;除非文件结束、编码错误或读取错误阻止了来自流的输入,在这种情况下,它是输入失败。输入项后面的第一个字符(如果有)保留未读。
10   除 % 指示符外,将输入项(或在 %n 指令的情况下,输入字符的计数)转换为适合于转换说明符的类型。 如果输入项不是匹配序列,则指令的执行失败:此条件是匹配失败。除非通过 * 指示了赋值抑制,否则将转换结果放置在跟随格式参数之后尚未接收到转换结果的第一个参数所指向的对象中。如果此对象没有适当的类型,或者转换的结果无法表示为该对象,则行为未定义。


310) fscanf 最多将一个输入字符推回输入流。因此,一些对 strtodstrtol 等可接受的序列对于 fscanf 是不可接受的。

在您的示例中,初始的 - 是匹配的输入序列的前缀,下一个字符,另一个 -,不匹配,因此保留在输入流中。 输入项 - 不是匹配序列,因此会出现转换失败,并返回 0,但第一个 - 已被消耗。

在GNUlibc上观察到此行为,但在Apple Libc上的macOS上不会消耗初始破折号。


"0xx对于%i的转换将导致转换失败" --> “int d; printf(”%d \ n“,sscanf(”0xx“,”%i“,&d));”对您来说打印什么。我得到1。与fscanf()相同。 - chux - Reinstate Monica
1
@chux-ReinstateMonica:我将删除这个示例,我得到了与您相同的结果,但是在macOS上,int d; char buf[10]; int res = sscanf("0xx", "%i%s", &d, buf); printf("%d %d %s\n", res, d, buf); 给出了不同的输出(2,0,xx),而在Linux上则是(2,0,x)。fscanf() 在 macOS 上也无法从 --1 中消耗 - - chqrlie
1
我必须说,任何依赖这种行为的代码编写者——也就是说,任何会因为苹果公司的不符合而受到不便的人——都是在自找麻烦... - Steve Summit
1
@SteveSummit:确实如此,但问题并不是因为人们故意依赖边缘情况的行为而引起的,而更可能是他们的代码在一个平台上能够运行而在另一个平台上不能运行,寻找这个边缘情况是一场噩梦。 - chqrlie
1
Charlie,我认为C规范在超过1个字符的推回方面不够精确,实现方式也因此而异。@SteveSummit是正确的——fscanf()并不是最好的工具:将一行读入字符串,然后解析字符串是最健壮的解决方案。 - chux - Reinstate Monica
显示剩余3条评论

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