5

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

以下代码足以说明问题:

#include <stdio.h>

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

    FILE *file = fopen("file.bin", "rb");
    fscanf(file, "%u", &num1);
    fgetc(file); // consume and discard \0
    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正好包含512\0256\0128\0

$ 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 标准。

4

2 回答 2

8

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


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

在 Windows 上使用 Visual Studio 编译它

假设 C99 标准。

据我所知,没有任何版本的 MSVC 符合 C99。最新版本在符合 C2011 方面可能做得更好,部分原因是 C2011 使一些在 C99 中是强制性的功能成为可选功能。

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

转换规范按以下步骤执行:

[...]

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

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

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

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

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

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

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

于 2017-02-23T16:49:33.327 回答
2

的 MSVC 实现fscanf显然是“垃圾”NUL旁边的字符512

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

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

对于除 n 之外的每个转换说明符,不超过任何指定字段宽度并且正是转换说明符所期望的或者是它所期望的序列的前缀的最长输入字符序列是从流中消耗的。在这个消费序列之后的第一个字符(如果有的话) 仍然是未读的。

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

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

规范说,只有当字符由isspace属性标识时才会发生这种情况,经过检查,该属性不在此处保留(即isspace('\0')产生 0)。

一种在 MSVC 和 GCC 中都有效的 hacky、类似正则表达式的解决方法可能是替换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
于 2017-02-23T16:49:47.403 回答