fscanf() != EOF 作为循环的退出条件,在错误的时间退出

fscanf() != EOF as exit condition of loop exits at the wrong time

提问人:fulcus 提问时间:8/17/2019 最后编辑:chqrliefulcus 更新时间:8/18/2019 访问量:198

问:

我从这个文件中获取输入,我知道它包含一系列类型的元素:

typedef struct{
    char artist[50];
    char title[50];
    int num; //numero traccia
    int minutes;
    int seconds;
} track;

但是当我用循环获取我的输入时,程序认为它紧跟在第一个结构之后,即使我发现其中有 9 个元素,然后是一堆垃圾。因此,在最坏的情况下,它应该在 9 个轨道之后打印一堆垃圾,而不是只读取第一个,然后认为它击中了.EOFEOF

起初我有,但我读到它真的很糟糕的代码,所以我尝试了每个人似乎都建议的返回,但它仍然不起作用。while(!feof(fp))fscanf

#include <stdio.h>
#define N 15

typedef struct {
    char artist[50];
    char title[50];
    int num; 
    int minutes;
    int seconds;
} track;

int main() {
    FILE *fp = fopen("/path/album.bin", "rb+");
    if (fp == NULL) {
        printf("Error opening file\n");
        return -1;
    }

    int i = 0;
    track song[N];    //array of structs

    while (fscanf(fp,"%[^\n] %[^\n] %d %d %d",
                  song[i].artist, song[i].title, &song[i].num,
                  &song[i].minutes, &song[i].seconds) != EOF) { 
        printf("Artist: %s\nTitle: %s\nNum: %d\nLength: %d:%d\n\n",
               song[i].artist, song[i].title, song[i].num,
               song[i].minutes, song[i].seconds);
        ++i;
    }
    fclose(fp);
    return 0;
}

输出为:

Artist: Frank Zappa
Title: Inca Roads
Num: 1
Length: 8:45

而它实际上应该打印相同格式的其他 8 首曲目。

C 循环文件 -IO 扫描 EOF

评论

1赞 Antti Haapala -- Слава Україні 8/17/2019
fscanf除了 !EOF
3赞 Antti Haapala -- Слава Україні 8/17/2019
此外,您正在阅读的文件是二进制文件,仅在文本上有意义。fscanf
2赞 Weather Vane 8/17/2019
题外话:根据你想要的结果来检查结果,而不是你不希望它成为什么。也就是说,扫描的项目数。fscanf
1赞 Weather Vane 8/17/2019
请在问题本身添加一个示例文本文件:链接不好 - 哦!它根本不是文本文件。
0赞 fulcus 8/17/2019
@AnttiHaapala您还推荐什么其他功能? ?@WeatherVane我会的,感谢您的输入。不过,我不确定为什么它不起作用fgetc()

答:

0赞 the busybee 8/17/2019 #1

既然你没有问一个具体的问题,我会尽量让你朝着正确的方向前进。

您链接的文件是一个二进制文件,其中包含 的记录。所以你不能用 .您可以用作标准替代方案;请阅读其文档,了解如何使用它以及返回的内容。如果文件在一台计算机上写入并在另一台计算机上读取,则考虑字节序。struct trackfscanf()fread()

评论中提到了另一个明显的问题:函数族返回成功扫描的字段数。如果出现错误,它将返回一个低于预期的值。scanf()int

0赞 Aconcagua 8/17/2019 #2

首先,您显然尝试读取二进制数据,请参阅 busybee 对此问题的回答(fscanf 不适用于二进制数据!

其次(现在假设数据存储在文本文件中),fscanf 不会返回文件流状态,而是返回成功扫描的参数数,因此您应该检查格式参数的数量是否相等,即 就你而言。5

如果您还想检查是否已到达文件末尾,则可以使用带有指针的 feof 作为参数。但请注意,阅读也可能因其他原因而失败。然后,您的代码可能如下所示:FILE

while(fscanf(...) == 5) { /* ... */ }
if(feof(fp))
{
    // you read entire file
}
else
{
    // something went wrong
}

也许只剩下空格了,所以你可能需要在最终检查之前注意是否已经到达文件末尾。

4赞 Frankie_C 8/17/2019 #3

你不能用来读取二进制文件,而你的文件是二进制文件。fscanf()

了解文件的二进制格式后,您可以复制结构并使用 .fread()

分析文件,我们可以看到结构没有填充,因此使用打包属性,我们可以强制编译器进行相同的填充。

然后,代码将如下所示:

#include <stdio.h>
#include<stdint.h>    //Include this header to access standard integer types as int32_t
#define N 15

#ifdef (__GNUCC__)
#define PACK  __attribute__((packed))
#else
#define PACK
#pragma pack(1)     //Use this if MS compiler or compatible
#endif

typedef struct
{
    char    artist[50];
    char    title[50];
    int32_t num;      //Note use of int32_t to force the use of 4bytes ints
    int32_t minutes;
    int32_t seconds;
} track PACK;

int main(int argc, char *argv[])
{
  FILE *fp = fopen("/path/album.bin","rb+");
  if(fp == NULL)
  {
    printf("Error opening file\n");
    return -1;
  }

  int i = 0;
  track song[N];    //array of structs

  // Note that fread returns 1 if a complete structure has been read
  // If the file contains less bytes of the size of the structure,
  // fread() will return 0 ending the input.
  while(fread( &song[i], sizeof(track), 1, fp)) 
  {
    printf("Artist: %s\nTitle: %s\nNum: %d\nLength:%d:%d\n\n",
            song[i].artist, song[i].title, song[i].num,
                             song[i].minutes, song[i].seconds);
    ++i;
  }

  fclose(fp);
  return 0;
}

编辑:请注意,使用该类型强制使用 32 位(4 字节)整数。这对于在标准 int 类型为 <> 32 位的系统上保持一致的结构布局是绝对强制性的。int32_t

观察文件的布局,我们可以发现一个类似于所提供结构的重复模式:

Offset  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ __________ _
0000:  |c|h|a|r| |a|r|t|i|s|t|[|5|0|]| | | / .... / | | 50bytes
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+----------+-+
0032:  |c|h|a|r| |t|i|t|l|e|[|5|0|]| | | | / .... / | | 50bytes
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+----------+-+
0064:  | | | | |    int num     = 32bits = 4bytes 
       +-+-+-+-+
0068:  | | | | |    int minutes = 32bits = 4bytes
       +-+-+-+-+
006C:  | | | | |    int seconds = 32bits = 4bytes
       +-+-+-+-+

structure size=50+50+4+4+4=112 bytes

通过观察它,我们看到这些字段没有被任何填充交错(至少考虑到 的标准大小。int32_t

另一方面,在没有打包属性的情况下对结构进行编码将使编译器可以自由地最终添加依赖于实现的填充,如果内存布局与文件布局不一致,可能会导致失败。

绕过此类问题的另一种可能性是序列化输入,在这种情况下,您将使用其长度对每个字段进行二进制读取。

关于使用,再多说一句(在此处查看更多信息 http://www.cplusplus.com/reference/cstdio/fread/)。设置 和 意味着 fread() 必须为每个元素读取至少大小的字节,并且只能读取一个元素。如果文件在可以读取该字节数之前到达末尾,则该函数将返回 0 个元素,即 false。如果可以读取完整元素,则该函数返回 1 个元素 read for true。size_t fread ( void * ptr, size_t size, size_t count, FILE * stream )count=1size=sizeof(track)

最后,要注意 endianess。在您的文件中,如果您的机器是结构成员的 big-endian 交换字节,则它是 little-endian 格式。int

评论

0赞 fulcus 8/17/2019
对不起,我不太确定你在那里做了什么。你怎么知道结构没有填充?打开文件会显示一堆 \00(那是什么?),hexdump -C 打印姓名和艺术家,然后是一堆点,这让我认为它是填充的,因为数据不是连续的。你介意澄清一下吗?
1赞 Frankie_C 8/17/2019
亲爱的海盗,看看你的结构,我们计算 2 个字符串成员,每个 50 字节宽,然后是 3 个 32 位或 4 个字节。只需添加,您就有 50*2+4*3=112 字节的每条记录。没有填充,因为字段之间没有未使用的空格。文件中每 112 个字节,整个结构就会重新启动。您可以使用十六进制编辑器来检查内部文件。您看到的零是填充,但属于单个结构成员,而不是结构本身。此外,在最后 2 条记录中,该成员是......int
1赞 Frankie_C 8/17/2019
到处都是垃圾。但你不必担心,因为有趣的数据在第一个零字节(C 字符串的结尾)停止。另一方面,甚至可能发生 3 个数字字段比 32 位短,并且结构填充在 32 位边界上,但即使在这种情况下,系统仍然可以工作,除非填充不包含与零不同的数据(在这种情况下,您可以屏蔽它们)。无论如何,您必须使用打包结构,因为任何其他类型都无法尊重文件上的结构布局(填充方式取决于编译器)。因此,对于一个好的逆向工程,你必须......
0赞 Frankie_C 8/17/2019
仔细观察,尝试多学习一点C语言。Buona Fortuna ;-)
0赞 Antti Haapala -- Слава Україні 8/18/2019
你不需要任何填充物,结构是(信不信由你)自然包装的。最值得注意的是,它必须最多 4 字节对齐。int32_t
1赞 John Bollinger 8/17/2019 #4

这里有趣的是,程序只发出一条记录:这是因为(二进制)文件中没有换行符,因此尝试使用整个文件内容来处理格式的第一个指令,第一次被调用。%[^\n]fscanf

不,有趣的是,尽管读取出了可怕的错误,但它发出的一条记录似乎是明智的。也就是说,由于前 50 个字符内没有换行符,因此扫描第一个字段会超出数组的边界,从而产生未定义的行为。song[0].artist

沉迷于对该 UB 表现形式的一些猜测,结果似乎是好像程序只是将文件的所有字节写入数组的表示中(该数组足够长以容纳它们),并且结构的布局恰好与文件的二进制格式相匹配(这并不奇怪)。结果,第一首曲目的所有字段似乎都已正确填充。songtrack

此外,轨道结构的细节使得它不太可能使用任何尾部(或内部)填充进行布局,因此我们甚至可以推测,在循环之外打印一些其他轨道也可能会产生预期的数据。

有趣的是,在一个系统上,它与写入文件的系统具有相同的字节序和相同的结构布局约定,并且假设结构确实是没有任何填充的布局,读取数据的最简单正确方法模拟了上述可能的 UB 特征:fscanf

// no loop needed if all our assumptions are satisfied
size_t num_songs = fread(song, sizeof(song[0]), N, fp);

这只是将整个文件(直到轨道)直接读取到数组的表示中。然后,您可以循环打印结果:N

for (int i = 0; i < num_songs; i++) {
    printf("Artist: %s\nTitle: %s\nNum: %d\nLength: %d:%d\n\n",
            song[i].artist, song[i].title, song[i].num, song[i].minutes,
            song[i].seconds);
}

这恰好对我有用。

评论

0赞 the busybee 8/17/2019
没有必要放在括号里。 就像一个一元算子,确定所操作对象的大小。因为我们经常把一个强制转换放在那里,所以它看起来像一个函数,例如。song[0]sizeofsizeof (int)
0赞 John Bollinger 8/17/2019
是的,@thebusybee,是一名操作员。在这种情况下,我使用的括号不是强制性的。但这不是我想与OP讨论的问题之一,OP几乎可以肯定习惯于看到括号。sizeof
0赞 the busybee 8/18/2019
没错,我只是希望在示例中看到好的代码。
0赞 John Bollinger 8/19/2019
尽管括号可以省略,@thebusybee,它们是如此惯用,以至于我完全拒绝这样做会使代码在任何对我来说重要的方式上变得更好的命题。
0赞 the busybee 8/19/2019
唉... ;-)没有冒犯。