ftell 如何影响以“r”而不是“rb”模式读取的二进制文件?

Cha*_*ase 6 c binary file cs50

我有一个相当奇怪的问题,实际上根本不是很实用。错误(以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;
}
Run Code Online (Sandbox Code Playgroud)

现在,card.raw是一个二进制文件,所以这个读取出错,因为是以rmode 而不是rb. 但我很好奇的是,该循环正好执行了 3 次,在最终执行中,它甚至没有读取 512 个字节。

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

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

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

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

我决定稍微改变循环以提取更多信息-

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

如果ftell包含在循环中并且前几个结果看起来像 -

查询结果

现在如果我完全删除那条ftell线,它会给我 -

没有 ftell 结果

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

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

注意:我知道两者都返回的计数fread并且ftell由于读取模式可能是错误的,但这不是我关心的问题。我只是好奇 - 为什么包括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 
Run Code Online (Sandbox Code Playgroud)

编辑:另外,我确实检查ferror了循环内部,有和没有ftell,根本没有错误。事实上,feof在这两种情况下,循环后都返回 1。

编辑:我也尝试memcard == NULLfopen相同的行为之后添加一个检查。

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

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

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

编辑:我以为我们已经得到了答案,它正在ftell重置 EOF。但我改变了循环——

/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 
Run Code Online (Sandbox Code Playgroud)

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

正如预期的那样,如果我将 更改ftellfseek(memcard, 0, SEEK_CUR)EOF 重置并且reached after永远不会打印 。

Apl*_*123 5

正如一些评论者指出的那样,它遇到了EOF, 并且ftell实际上摆脱了 EOF。为什么?为了找到答案,我们必须查看 glibc 的源代码。我们可以找到以下来源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)
Run Code Online (Sandbox Code Playgroud)

这是重要的一行:

pos = _IO_seekoff_unlocked (fp, 0, _IO_seek_cur, 0);
Run Code Online (Sandbox Code Playgroud)

让我们找到_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);
}
Run Code Online (Sandbox Code Playgroud)

基本上,它只是做一些检查然后调用_IO_SEEKOFF,所以让我们找到它的来源

/* 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)
Run Code Online (Sandbox Code Playgroud)

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