可以通过fseek()读取整个文件到SEEK_END并通过ftell()获取文件大小吗?

时间:2017-04-25 08:58:45

标签: c stream undefined-behavior file-read

我是否认为此代码引入了未定义的行为?

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

FILE *f = fopen("textfile.txt", "rb");
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);  //same as rewind(f);

char *string = malloc(fsize + 1);
fread(string, fsize, 1, f);
fclose(f);

string[fsize] = 0;

我之所以要问的是,此代码是作为以下问题的可接受且高度评价的答案发布的:C Programming: How to read the whole file contents into a buffer

然而,根据以下文章:How to read an entire file into memory in C++(尽管它的标题,也涉及C,所以坚持我):

  

假设您正在编写C,并且您有FILE*(您知道点数   到文件流,或至少是可搜索的流),你想要   确定在缓冲区中分配多少个字符来存储   流的全部内容。你的第一直觉可能是   写这样的代码:

// Bad code; undefined behaviour
fseek(p_file, 0, SEEK_END);
long file_size = ftell(p_file);
     

似乎合法。但后来你开始变得怪异了。有时候   报告的大小大于磁盘上的实际文件大小。有时   它与实际文件大小相同,但是字符数   你读的是不同的。到底是怎么回事?

     

有两个答案,因为它取决于文件是否已经存在   以文本模式或二进制模式打开。

     

万一你不知道区别:在默认模式下 - 文本   模式 - 在某些平台上,某些字符被翻译成   阅读中的各种方式。最着名的是在Windows上,   当写入文件时,换行符被转换为\r\n   阅读时翻译了另一种方式。换句话说,如果是文件   包含Hello\r\nWorld,它将被视为Hello\nWorld;文件   大小是12个字符,字符串大小是11.不太知名的是   0x1A(或Ctrl-Z)被解释为文件的结尾,因此如果是文件   包含Hello\x1AWorld,它将被视为Hello。另外,如果   内存中的字符串是Hello\x1AWorld,然后将其写入文件中   在文本模式下,文件将为Hello。在二进制模式下,没有   翻译完成 - 文件中的任何内容都会被读入您的文件   程序,反之亦然。

     

你可以立刻猜到文字模式会很头疼 -   在Windows上,至少。更一般地说,根据C标准:

     
    

ftell函数获取stream指向的流的文件位置指示符的当前值。对于二进制流,     该值是文件开头的字符数。     对于文本流,其文件位置指示符包含未指定     信息,可由fseek函数用于返回文件     流的位置指示器到达时的位置     ftell call;两个这样的返回值之间的差异不是     必然是一个有意义的措施,写入的字符数     或阅读。

  
     

换句话说,当您处理以文本模式打开的文件时,   ftell()返回的值无效......除了调用fseek()之外。   特别是,它不一定告诉你有多少个字符   在流中直到当前点。

     

因此,您无法使用ftell()的返回值来告诉您大小   文件,文件中的字符数或任何内容   (稍后调用fseek()除外)。所以你无法获得文件大小   那样。

     

好的,所以到了文本模式的地狱。有什么说我们只在二进制模式下工作?   正如C标准所说:“对于二进制流,值是数字   从文件开头的字符。“这听起来很有希望。

     

事实上确实如此。如果您在文件的末尾,并且您致电   ftell(),您将找到文件中的字节数。好哇!   成功!我们现在需要做的就是到文件的末尾。并   这样做,你需要做的只是fseek()SEEK_END,对吗?

     

错误。

     

再次,从C标准:

     
    

将文件位置指示符设置为文件结尾,与fseek(file, 0, SEEK_END)一样,具有二进制流的未定义行为     (因为可能是尾随空字符)或任何流有     状态相关的编码,不能确定地在初始结束     转变状态。

  
     

要理解为什么会这样:某些平台将文件存储为   固定大小的记录。如果文件短于记录大小,则   块的其余部分是填充的。当你寻求“结束”,为   效率的缘故它只会让你跳到最后   阻止...可能在数据实际结束后很久,一堆之后   填充。

     

所以,这是C中的情况:

     
      
  • 在文字模式下,您无法获得ftell()的字符数。
  •   
  • 您可以在二进制模式下使用ftell()获取字符数...但您无法使用fseek(p_file, 0, SEEK_END)搜索文件的末尾。
  •   

我没有足够的知识判断谁在这里,如果上述接受的答案确实与本文发生冲突,那么我就是在问这个问题。

1 个答案:

答案 0 :(得分:4)

该文章的作者恶意忽略的是引用的上下文。

根据C11标准草案n1570,非规范脚注268

  

将文件位置指示器设置为文件结尾,如同   fseek(file,0,SEEK_END)具有二进制流的未定义行为   (因为可能是尾随空字符)或任何流有   状态相关的编码,不能确定地在初始结束   转变状态。

引用脚注的标准的规范部分是 7.21.3文件

  

9尽管文本和二进制广泛的流都是概念上的   宽字符序列,与a关联的外部文件   面向广义的流是一系列多字节字符,   概括如下:

     

- 文件中的多字节编码可能包含   嵌入的空字节(与多字节编码不同,对内部使用有效)   该计划)。

     

- 文件不需要在初始移位状态下开始或结束。 268)

请注意,这涉及面向广播的流

现在,在 7.21.9.2 fseek函数

  

3对于二进制流,新位置以字符为单位测量   文件的开头是通过向文件添加偏移量获得的   由whence指定的位置。指定的位置是开始   文件的当前值是SEEK_SET,即文件的当前值   位置指示符,如果SEEK_CUR,或结束文件,如果SEEK_END。二进制   stream不需要有意义地支持具有whence值的fseek调用   SEEK_END。

语言是一个相当不那么可怕的最后一句:

“二进制流无需支持具有SEEK_END值的fseek调用。”