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

时间:2017-02-23 16:08:30

标签: c scanf stdio null-character

我在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打印到stdout。
在Windows上使用MinGW 4.8.1进行编译会得到相同的预期结果。

然而,当我使用Visual Studio Community 2015编译代码时,似乎有一个主要的区别;即输出为:

512, 56, 28

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

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

这里发生了什么,如何将其转换为可移植的C代码?我是否遇到了未定义的行为?请注意,我假设采用C99标准。

2 个答案:

答案 0 :(得分:8)

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

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

的组合
  

使用Windows上的Visual Studio编译它

  

假设C99标准。

据我所知, 没有 版本的MSVC符合C99。最近的版本可能会更好地符合C2011,部分原因是因为C2011使得某些功能在C99中是强制性的。

无论您使用哪种版本的MSVC,我认为它都不符合该领域的标准(C99和C2011)。以下是C99, section 7.19.6.2

的相关文字
  

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

     

[...]

     

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

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

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

  

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

“strtoul函数的主题序列”定义为in that function's specifications

  

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

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

答案 1 :(得分:2)

fscanf的MSVC实施显然是&#34;垃圾&#34; NUL旁边的512字符:

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

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

  

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

请注意,这与人们希望跳过尾随白色字符的情况不同,如下面的陈述所述:

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

规范说,只有当isspace属性标识字符时才会发生这种情况,如果已检查,则不会保留(即isspace('\0')产生0)。

在MSVC和GCC中都适用的hacky,类似regex的解决方法可能是将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