在使用'r'模式而非'rb'模式读取二进制文件时,ftell会产生什么影响?

6

我有一个相当好奇的问题,实际上并不是很实用。这个错误(在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 results

现在如果我完全删除ftell语句,它就会给我这个-

without ftell results

只有3次执行,但没有任何变化。
这种行为背后的解释是什么?
注:我知道由于读取模式,freadftell返回的计数可能不正确,但这不是我的关注点。我只是好奇——为什么包括ftell和不包括它之间会有差异。
此外,如果有帮助的话,《card.raw》文件实际上只是cs50 pset4“memory card”。可以通过wget https://cdn.cs50.net/2019/fall/psets/4/recover/recover.zip来获取它,并将输出文件存储在一个.zip文件中。
编辑: 我应该提到这是在Windows下使用clang工具的VS2019。命令行选项(从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永远不会被打印。


1
现在,card.raw是一个二进制文件,因此如果以r模式而不是rb模式读取,那么读取可能会出错。但这并不一定会出错,这取决于您的平台。 - Cheatah
1
@chux-ReinstateMonica 抱歉,我不知道它是怎么出现在问题中的。我在测试中没有使用 &,不用担心。 - Chase
1
“在最终执行中,它甚至没有读取512个字节。”和“The fread count is still messed up.”让我担心的是代码while (fread(buffer, sizeof(*buffer), BUFFER_LEN, memcard) != 0)没有更新count,因此这些结论基于除发布的代码之外的其他内容。我建议在每个步骤中发布使用的确切代码以改进调查。 - chux - Reinstate Monica
3
没有 ftell,它会在遇到 Windows 文本模式文件中的 EOF 字符 0x1a 时停止。我不知道为什么调用 ftell 会改变这个行为。 - interjay
1
@chux-ReinstateMonica bingo!刚才我在循环中加入了feof,这就是问题所在。它确实被执行并且ftell似乎将其重置。因此,0x1aftell的组合导致了问题。您能否在一个答案中解释整个过程? - Chase
显示剩余16条评论
2个回答

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)

这是重要的一行:

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

让我们找到与_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,因此让我们找到它的源代码

/* 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,因为“文件结束指示器”仍应设置。 - chux - Reinstate Monica
有趣的是,似乎ftello允许重置文件上的错误,但ftell不行...? https://pubs.opengroup.org/onlinepubs/9699919799/functions/ftell.html - Chase
我没有找到关于“ftello可以重置文件错误”的支持。ftello()ftell()都可以设置errno,但这不是由ferror()报告的流的错误指示器。总之,这两个函数都没有隐含的能力来清除文件结束标志。 - chux - Reinstate Monica
坏消息是,ftell 没有重置 EOF - 我在 ftell 后面加了另一个 if(feof(memcard)),但它仍然被触发。fread 在那个“假”的 EOF 之后仍然继续读取,我们回到原点了吗? - Chase

1

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

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

count < BUFFER_LEN时,OP报告feof()为true-如预期。

意外的是后续的fread()返回非零值。

在我看来,这是一个不符合规范的库。

(OP报告了新信息,因此此答案现在不完整。)

似乎ftell()错误地重置了流的文件结束指示器,导致可以进行其他读取。


坏消息,ftell 没有重置 EOF - 我在 ftell 后面放了另一个 if(feof(memcard)),但它仍然被触发。 fread 在那个“虚假”的 EOF 后面以某种方式继续读取,我们回到了原点..? - Chase
@Chase "ftell不会重置EOF" --> 这很好,因为它不应该这样做。C语言有“字节输入函数从流中读取字符,就像通过连续调用fgetc函数一样。”所以在短暂的fread()之后,下一个fread()应该返回0,因为该调用就像512个fgetc()。而且,fgetc()有“如果设置了流的文件结束指示器,或者流已经到达文件结束,则设置流的文件结束指示器,并且fgetc函数返回EOF”。好奇的是,在短暂的fread()之后,fgetc()返回什么?然后是feof(), ferror()呢? - chux - Reinstate Monica
没有 ftell,即在第三次迭代的 shortread 中,fgetc 返回 -1,ferror 仍为 0,而 feof 为 1。 - Chase
ferror gives 0, feof gives 1, in all situations, before ftell, after ftell, before fgetc and after fgetc - Chase
嗯,ftell() 正在清除一些东西,而 fread() 不兼容。当 feof() 为真时,fread() 应该返回0。顺便问一下,“突然没有 -1”,值是多少?26? - chux - Reinstate Monica
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接