【问题标题】:How does ftell affect a binary file being read in mode 'r' instead of 'rb'?ftell 如何影响以 'r' 而不是 'rb' 模式读取的二进制文件?
【发布时间】:2020-09-29 18:37:59
【问题描述】:

我有一个相当奇怪的问题,实际上一点也不实用。错误(在r 模式下读取二进制文件)显而易见,但我被别的东西弄糊涂了。

这是代码-

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdint.h>

#define BUFFER_LEN 512

typedef uint8_t BYTE;

int main()
{
    FILE* memcard = fopen("card.raw", "r");
    BYTE buffer[BUFFER_LEN];
    int count = 0;
    while (fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard) != 0)
    {
        printf("count: %d\n", count++);
    }
    fclose(memcard);
    return 0;
}

现在,card.raw 是一个二进制文件,因此由于在 r 模式而不是 rb 中读取,此读取出错。但我很好奇的是,那个循环恰好执行了 3 次,在最后的执行中,它甚至没有读取 512 个字节。

现在如果我将该循环更改为

while (fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard) != 0)
{
    printf("ftell: %ld\n", ftell(memcard));
}

它不再在 3 次处决时停止。事实上,它一直持续到(大概)文件结束。 fread 计数仍然混乱。许多读取不会在读取元素时返回 512。但这很可能是由于文件以r 模式打开以及伴随的所有编码错误。

ftell 不应该影响文件本身,那么为什么在循环中包含ftell 会使它执行更多次?

我决定对循环进行更多更改以提取更多信息-

while ((count = fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard)) != 0)
{
    printf("fread bytes read: %d\n", count);
    printf("ftell: %ld\n", ftell(memcard));
}

如果ftell 包含在循环中并且前几个结果看起来像这样,则此循环的次数与它一样多-

现在,如果我完全删除 ftell 行,它会给我-

只有 3 次处决,但没有任何改变。

这种行为背后的解释是什么?

注意:我知道 freadftell 返回的计数可能由于读取模式而出错,但这不是我关心的问题。我只是好奇 - 为什么有区别,包括 ftell 和不包括它。

另外,如果有帮助,card.raw 文件实际上只是 cs50 pset4“存储卡”。您可以通过wget https://cdn.cs50.net/2019/fall/psets/4/recover/recover.zip 获取它并将输出文件存储在.zip

编辑:我应该提到这是在 Windows 上使用 VS2019 的 clang 工具。命令行选项(从 VS2019 项目属性中检查)看起来像-

/permissive- /GS /W3 "Debug\" "Debug\" /Zi /Od "Debug\vc142.pdb" /fp:precise /D "_CRT_SECURE_NO_WARNINGS" /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /WX- /Gd /MDd /Fa"Debug\" /EHsc /nologo /Fo"Debug\" /Fp"Debug\Test.pch" /diagnostics:column 

编辑:另外,我确实检查了循环内的ferror,无论有无ftell,都没有得到任何错误。事实上,feof 在循环之后返回 1,在这两种情况下。

编辑:我还尝试在 fopen 之后添加一个 memcard == NULL 检查,同样的行为。

编辑:解决@orlp 的答案。事实上,我确实检查了错误。不过我肯定应该发布它。

while ((count = fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard)) != 0)
{
    if ((err = ferror(memcard)))
    {           
        fprintf(stderr, "Error code: %d", err);
        perror("Error: ");
        return 1;
    }
    printf("fread bytes read: %d\n", count);
    printf("ftell: %ld\n", ftell(memcard));
}
if ((err = ferror(memcard)))
{
    fprintf(stderr, "Error code: %d", err);
    perror("Error: ");
    return 1;

}

两个if 语句都没有被触发。

编辑:我以为我们已经得到了答案,它是 ftell 重置 EOF。但是我把循环改成了-

while ((count = fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard)) != 0)
{
    if ((err = ferror(memcard)))
    {
        fclose(memcard);
        fprintf(stderr, "Error code: %d", err);
        perror("Error: ");
        return 1;
    }
    if (feof(memcard))
    {
        printf("reached before\n");
    }
    printf("fread bytes read: %d\n", count);
    ftell(memcard);
    if (feof(memcard))
    {
        printf("reached after\n");
    }
}

这会触发第一个if(feof) 和第二个if(feof)

不过,正如预期的那样,如果我将 ftell 更改为 fseek(memcard, 0, SEEK_CUR)EOF 重置,而 reached after 永远不会打印出来。

【问题讨论】:

  • > 现在,card.raw 是一个二进制文件,因此由于在r 模式而不是rb 模式下读取,此读取会出错。不,它不一定会出错。这取决于您的平台。
  • @chux-ReinstateMonica 抱歉,我不知道它是如何出现在问题中的。我在测试中没有使用&amp;,不用担心。
  • "在最终执行中,它甚至没有读取 512 个字节。"和“fread 计数仍然搞砸了。”让我担心的是代码while (fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard) != 0) 不会更新count,因此这些结论基于发布的代码以外的其他内容。我建议发布每个步骤使用的确切代码以改进调查。
  • 如果没有ftell,它会在遇到0x1a 字符时停止,这意味着Windows 文本模式文件中的EOF。我不知道为什么打电话给ftell 会改变这一点。
  • @interjay 嗯,非常有用的评论。

标签: c file binary cs50


【解决方案1】:

正如一些评论者所指出的,它遇到了EOF,而ftell 实际上摆脱了那个EOF。为什么?要找到答案,我们必须查看 glibc 的源代码。我们可以找到source for ftell::

long int
_IO_ftell (FILE *fp)
{
  off64_t pos;
  CHECK_FILE (fp, -1L);
  _IO_acquire_lock (fp);
  pos = _IO_seekoff_unlocked (fp, 0, _IO_seek_cur, 0);
  if (_IO_in_backup (fp) && pos != _IO_pos_BAD)
    {
      if (_IO_vtable_offset (fp) != 0 || fp->_mode <= 0)
    pos -= fp->_IO_save_end - fp->_IO_save_base;
    }
  _IO_release_lock (fp);
  if (pos == _IO_pos_BAD)
    {
      if (errno == 0)
    __set_errno (EIO);
      return -1L;
    }
  if ((off64_t) (long int) pos != pos)
    {
      __set_errno (EOVERFLOW);
      return -1L;
    }
  return pos;
}
libc_hidden_def (_IO_ftell)

weak_alias (_IO_ftell, ftell)

这是重要的一行:

pos = _IO_seekoff_unlocked (fp, 0, _IO_seek_cur, 0);

让我们找到source for _IO_seekoff_unlocked

off64_t
_IO_seekoff_unlocked (FILE *fp, off64_t offset, int dir, int mode)
{
  if (dir != _IO_seek_cur && dir != _IO_seek_set && dir != _IO_seek_end)
    {
      __set_errno (EINVAL);
      return EOF;
    }

  /* If we have a backup buffer, get rid of it, since the __seekoff
     callback may not know to do the right thing about it.
     This may be over-kill, but it'll do for now. TODO */
  if (mode != 0 && ((_IO_fwide (fp, 0) < 0 && _IO_have_backup (fp))
            || (_IO_fwide (fp, 0) > 0 && _IO_have_wbackup (fp))))
    {
      if (dir == _IO_seek_cur && _IO_in_backup (fp))
    {
      if (_IO_vtable_offset (fp) != 0 || fp->_mode <= 0)
        offset -= fp->_IO_read_end - fp->_IO_read_ptr;
      else
        abort ();
    }
      if (_IO_fwide (fp, 0) < 0)
    _IO_free_backup_area (fp);
      else
    _IO_free_wbackup_area (fp);
    }

  return _IO_SEEKOFF (fp, offset, dir, mode);
}

基本上,它只是做一些检查然后调用_IO_SEEKOFF,所以let's find its source

/* The 'seekoff' hook moves the stream position to a new position
   relative to the start of the file (if DIR==0), the current position
   (MODE==1), or the end of the file (MODE==2).
   It matches the streambuf::seekoff virtual function.
   It is also used for the ANSI fseek function. */
typedef off64_t (*_IO_seekoff_t) (FILE *FP, off64_t OFF, int DIR,
                      int MODE);
#define _IO_SEEKOFF(FP, OFF, DIR, MODE) JUMP3 (__seekoff, FP, OFF, DIR, MODE)

所以基本上,ftell 最终调用了一个等效于fseek(fp, 0, SEEK_CUR) 的函数。在fseek 标准中,我们看到:“成功调用fseek() 函数会清除流的文件结束指示符。”这就是ftell 改变程序行为的原因。

【讨论】:

  • 嗯,因为 C 标准没有提到 ftell() 清除 文件结束指示符,这似乎是不合规的行为。短读后的 fread() 调用应返回 0,因为仍应设置 文件结束指示符
  • 有趣,似乎允许ftello 重置文件上的错误,但不允许ftell...? pubs.opengroup.org/onlinepubs/9699919799/functions/ftell.html
  • 我没有找到对“允许 ftello 重置文件错误”的支持。 ftello()ftell() 都可以设置errno,但这不是ferror() 报告的流的错误指示符。 IAC,这两个函数都没有隐含的能力来清除文件结束指示符。
  • 坏消息,ftell 没有重置 EOF - 我在 ftell 之后添加了另一个 if(feof(memcard)),它仍然被触发。 fread 即使在那个“假”EOF 之后仍在继续阅读,我们又回到了第一格......?
【解决方案2】:

fread()

fread函数返回成功读取的元素个数,如果遇到读取错误或文件结尾,可能小于nmemb。

count &lt; BUFFER_LEN 时,OP 报告feof() 为真 - 正如预期的那样。

出乎意料的是后面的fread() 返回非零。

IMO,一个不合规的库。

(OP 报告了新信息,所以这个答案现在不完整。)

出现ftell(),IMO 错误,重置流的文件结束指示符,允许进行额外读取。

【讨论】:

  • 坏消息,ftell 没有重置 EOF - 我在 ftell 之后添加了另一个 if(feof(memcard)),它仍然被触发。 fread 即使在那个“假”EOF 之后仍在继续阅读,我们又回到了第一格......?
  • @Chase "ftell 没有重置 EOF" --> 很好,因为它不应该这样做。 C 有“字节输入函数从流中读取字符,就像通过连续调用 fgetc 函数一样。”所以在短暂的fread() 之后,下一个fread() 应该返回0,因为那个调用就像512 fgetc()。并且fgetc() 具有“如果设置了流的文件结束指示符,或者如果流处于文件结束位置,则流的文件结束指示符为set 并且 fgetc 函数返回 EOF。很好奇,在短暂的fread() 之后,fgetc() 返回什么?然后feof(), ferror()
  • 没有ftell,即第3次迭代的shortread,fgetc返回-1,ferror仍然是0,feof是1
  • ftell,如果我做fgetc 之前 ftell,在短读之后它给我-1,我在ftell之后做,突然没有 -1
  • ftell()feof(), ferror()fgetc()之前和之后是什么?
猜你喜欢
  • 2017-05-30
  • 2016-09-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-29
  • 1970-01-01
相关资源
最近更新 更多