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

gaa*_*kam 7 c stream file-read undefined-behavior

我是对的,这段代码引入了未定义的行为?

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

我之所以要问的是,这段代码是作为以下问题的一个被接受且高度评价的答案发布的:C编程:如何将整个文件内容读入缓冲区

但是,根据以下文章:如何在C++中读取整个文件到内存中(尽管它的标题,它也处理C,所以坚持我):

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

// Bad code; undefined behaviour
fseek(p_file, 0, SEEK_END);
long file_size = ftell(p_file);
Run Code Online (Sandbox Code Playgroud)

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

有两个答案,因为它取决于文件是以文本模式还是以二进制模式打开.

万一你不知道区别:在默认模式 - 文本模式 - 在某些平台上,某些字符在阅读过程中会以各种方式被翻译.最着名的是,在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调用时的位置; 两个这样的返回值之间的差异不一定是写入或读取的字符数的有意义的度量.

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

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

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

确实如此.如果您位于文件末尾并且调用 ftell(),则会在文件中找到字节数.好哇!成功!我们现在需要做的就是到文件的末尾.要做到这一点,所有你需要做的是fseek()SEEK_END,对不对?

错误.

再次,从C标准:

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

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

那么,这是C中的情况:

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

我没有足够的知识判断谁在这里,如果上面提到的答案确实与这篇文章发生冲突,那么我就是在问这个问题.

EOF*_*EOF 4

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

\n

来自 C11 标准草案 n1570,非规范脚注 268

\n
\n

将文件位置指示符设置为文件结尾(与 \nfseek(file, 0, SEEK_END) 一样)对于二进制流\n(因为可能存在尾随空字符)或任何状态相关的流具有未定义的行为\n不一定以初始\nshift 状态结束的编码。

\n
\n

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

\n
\n

9 虽然文本和二进制宽导向流在概念上都是宽字符序列,但与宽导向流关联的外部文件是多字节字符序列,概括如下:

\n

\xe2\x80\x94 文件内的多字节编码可能包含嵌入的空字节(与程序内部有效使用的多字节编码不同)。

\n

\xe2\x80\x94 文件不需要以初始移位状态开始或结束。268)

\n
\n

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

\n

现在,在7.21.9.2 中 fseek 函数

\n
\n

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

\n
\n

最后一句的语言不那么可怕:

\n

“二进制流不需要有意义地支持带有 SEEK_END 值的 fseek 调用。”

\n