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

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

我正在从 this file 获取输入,我知道它包含一系列类型的元素:

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

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

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

#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 个相同格式的其他曲目。

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

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

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

起初,您显然尝试读取二进制数据,请参阅 busybee's 了解此问题(fscanf 不适用于二进制数据!)。

其次(现在假设数据将存储在 text 文件中),fscanf 不会 return 文件流状态,但是成功扫描的参数数量,所以你应该检查格式参数数量是否相等,i。 e. 5 你的情况。

如果您还想检查是否已到达文件末尾,您也可以使用 feof with the FILE pointer as argument. Be aware, though, that reading might fail for other reasons。您的代码可能看起来像这样:

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

可能还只剩下空白,因此您可能需要在最终检查是否已到达文件末尾之前注意一下。

您不能使用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;
}

编辑:注意类型int32_t的使用强制使用32位(4字节)整数。 这对于在标准 int 类型为 <> 32 位.

的系统上保持结构布局的一致性是绝对必要的

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

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.

的标准大小

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

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

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

最后,注意字节顺序。在你的文件中它是一个小端格式,如果你的机器是大端交换结构的int个字节的字节。

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

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

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

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

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

// 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);
}

这恰好适合我。