c语言 - 不同编译器之间 fscanf() 的行为不一致(使用尾随空字符)



我用C99编写了一个完整的应用程序,并在两个基于GNU/Linux的系统上对其进行了彻底的测试。当尝试在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 
fscanf(file, "%u", &num2);
fgetc(file); // ditto
fscanf(file, "%u", &num3);
fgetc(file); // ditto
fclose(file);
printf("%d, %d, %dn", num1, num2, num3);
return 0;
}

假设file.bin正好包含512256128

$ 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打印到 stdout。
在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函数读取二进制数据是一个有问题的计划的评论。 然而,更值得怀疑

的是

在Windows上使用Visual Studio编译它

假设 C99 标准。

据我所知,没有一个版本的 MSVC 符合 C99。 最新版本可能更好地符合 C2011,部分原因是 C2011 使某些功能成为 C99 中必需的可选功能。

但是,无论您使用哪个版本的MSVC,我认为它都不符合该领域的标准(C99和C2011)。 以下是C99第7.19.6.2节的相关文本

转换规范通过以下步骤执行:

[...]

从流 [...] 中读取输入项。输入项定义为最长的输入字符序列,该序列不超过任何指定的字段宽度,并且是匹配输入序列的前缀。输入项后的第一个字符(如果有)保持未读状态。

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

对于u转换说明符,匹配的输入序列定义为

匹配一个可选符号十进制整数,该整数的格式与 strtoul 函数的主题序列的预期格式相同,基参数的值为 10。

"strtoul 函数的主题序列"在该函数的规范中定义:

首先,它们将输入字符串分解为三部分:一个初始的、可能为空的空白字符序列(由 isspace 函数指定)、一个主题序列,类似于由 base 值确定的某个基数表示的整数,以及一个或多个无法识别的字符的最终字符串,包括输入字符串的终止 null 字符。

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

fscanf的 MSVC 实现显然是在"垃圾桶"512旁边的NUL字符:

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

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

对于除 n 以外的每个转换说明符,最长序列 输入不超过任何指定字段宽度的字符,以及 这要么正是转换说明符所期望的,要么是一个 它期望的序列的前缀是从 流。此使用序列之后的第一个字符(如果有)仍然未读

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

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

规范说,只有当字符由isspace属性标识时,才会发生这种情况,该属性在选中时不在此处成立(即,isspace('')产生 0)。

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

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

或者通过将实现定义的0-9字符类替换为文字数字来更便携:

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

最新更新