fscanf() 跨不同编译器的不一致行为(消耗尾随空字符)

Inconsistent behavior of fscanf() across different compilers (consuming trailing null character)

我用 C99 编写了一个完整的应用程序,并在两个 GNU/Linux-based 系统上对其进行了全面测试。当尝试在 Windows 上使用 Visual Studio 编译它导致应用程序行为异常时,我感到很惊讶。起初我无法断言哪里出了问题,但我尝试使用 VC 调试器,然后我发现了关于 stdio.h.

中声明的 fscanf() 函数的差异

下面的代码足以说明问题:

#include <stdio.h>

int main() {
    unsigned num1, num2, num3;

    FILE *file = fopen("file.bin", "rb");
    fscanf(file, "%u", &num1);
    fgetc(file); // consume and discard [=10=]
    fscanf(file, "%u", &num2);
    fgetc(file); // ditto
    fscanf(file, "%u", &num3);
    fgetc(file); // ditto
    fclose(file);

    printf("%d, %d, %d\n", num1, num2, num3);

    return 0;
}

假设 file.bin 恰好包含 5125628[=15=]:

$ hexdump -C file.bin
00000000  35 31 32 00 32 35 36 00  31 32 38 00              |512.256.128.|

现在,当在 Ubuntu 机器上使用 GCC 4.8.4 编译时,生成的程序会按预期读取数字并将 512, 256, 128 打印到标准输出。
在 Windows 上使用 MinGW 4.8.1 编译它给出了相同的预期结果。

但是,当我使用 Visual Studio Community 2015 编译代码时,似乎存在重大差异;即输出为:

512, 56, 28

如您所见,尾随空字符已被 fscanf() 使用,因此 fgetc() 捕获并丢弃对数据完整性至关重要的字符。

注释掉 fgetc() 行可以使代码在 VC 中运行,但在 GCC(可能还有其他编译器)中会破坏它。

这是怎么回事,如何将其转换为可移植的 C 代码?我遇到了未定义的行为吗?请注意,我假设是 C99 标准。

TL;DR:您被 MSVC 不一致性问题困扰了,这是一个长期存在的问题,MS 从未对解决它表现出太大兴趣。如果除了符合 C 实现之外还必须支持 MSVC,那么一种方法是在通过 MSVC 编译程序时使用条件编译指令来抑制 fgetc() 调用。


我倾向于同意通过格式化 I/O 函数读取二进制数据是一个有问题的计划的评论。然而,更值得怀疑的是

compil[ing] it using Visual Studio on Windows

assuming the C99 standard.

据我所知,no版本的MSVC符合C99。最新版本可能更好地符合 C2011,部分原因是 C2011 使一些在 C99 中强制性的功能成为可选功能。

但是,无论您使用的是哪个版本的 MSVC,我认为它都不符合这方面的标准(C99 和 C2011)。这是来自 C99, section 7.19.6.2

的相关文本

A conversion specification is executed in the following steps:

[...]

An input item is read from the stream [...]. An input item is defined as the longest sequence of input characters which does not exceed any specified field width and which is, or is a prefix of, a matching input sequence. The first character, if any, after the input item remains unread.

标准非常明确,与输入序列不匹配的第一个字符保持未读状态,因此可以认为 MSVC 符合标准的唯一方法是 [=11=] 个字符是否可以被解释为 (并终止)匹配的输入序列,或者如果 fgetc() 被允许跳过 [=11=] 个字符。我认为后者没有理由,特别是考虑到流是以二进制模式打开的,所以让我们考虑前者。

对于 u 转换说明符,匹配的输入序列是 defined 作为

Matches an optionally signed decimal integer, whose format is the same as expected for the subject sequence of the strtoul function with the value 10 for the base argument.

"subject sequence of the strtoul function"定义in that function's specifications:

First, they decompose the input string into three parts: an initial, possibly empty, sequence of white-space characters (as specified by the isspace function), a subject sequence resembling an integer represented in some radix determined by the value of base, and a final string of one or more unrecognized characters, including the terminating null character of the input string.

请特别注意,终止空字符明确归因于无法识别字符的最终字符串。它不是主题字符串的一部分,因此在根据 u 说明符转换输入时不应与 fscanf() 匹配。

fscanf 的 MSVC 实现显然是 "trashing" 512 旁边的 NUL 字符:

fscanf(file, "%u", &num1);

根据 fscanf 文档,这不应该发生(强调我的):

For every conversion specifier other than n, the longest sequence of input characters which does not exceed any specified field width and which either is exactly what the conversion specifier expects or is a prefix of a sequence it would expect, is what's consumed from the stream. The first character, if any, after this consumed sequence remains unread.

请注意,这与希望跳过 尾随 个白色字符的情况不同,如以下语句所示:

fscanf(file, "%u ", &num1); // notice "%u "

规范说,只有当字符由 isspace 属性 标识时才会发生这种情况,经检查,此处不成立(即 isspace('[=19=]') 产生 0 ).

一种在 MSVC 和 GCC 中都有效的类似正则表达式的 hacky 解决方法可能是将 fgetc 替换为:

fscanf(file, "%*1[^0-9+-]"); // skip at most one non-%u character

或更方便地替换 实现定义的 0-9 字符 class 为文字数字:

fscanf(file, "%*1[^0123456789+-]"); // skip at most one non-%u character