我在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标准。
答案 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