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

Posted

技术标签:

【中文标题】可以通过 fseek() 读取整个文件到 SEEK_END 并通过 ftell() 获取文件大小吗?【英文标题】:Possible to read a whole file by fseek()ing to SEEK_END and obtaining the file size by ftell()? 【发布时间】:2017-09-22 04:38:55 【问题描述】:

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

#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 函数获取流指向的流的文件位置指示符的当前值。对于二进制流, 该值是从文件开头开始的字符数。 对于文本流,其文件位置指示符包含未指定 信息,可由 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) 查找文件末尾。

我没有足够的知识来判断谁在这里,如果前面接受的答案确实与本文冲突,所以我问这个问题。

【问题讨论】:

一件事,你没有检查malloc()的返回值,如果失败,你就有了UB。 @SouravGhosh 当然可以,但这不是这里的核心问题。 正确,这就是为什么它是评论,而不是答案。 :) 见this answer。这是未定义的行为。所以它不是便携式的。 最健壮和便携的方法仍然是读取字符直到 EOF 并计算它们。 (当您使用它时,您可以将它们存储到一个数组中并在需要时调整数组的大小) 【参考方案1】:

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

来自 C11 草案标准 n1570,NON-NORMATIVE FOOTNOTE 268

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

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

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

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

——文件不需要以初始移位状态开始或结束。第268章)

请注意,这涉及面向宽的流

现在,在 7.21.9.2 中的 fseek 函数

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

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

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

【讨论】:

C 被设计为即使在执行相当奇怪和奇怪的事情的文件系统上也可以实现。如果文件系统不跟踪精确到字节的文件大小,则要求实现这样做可能会使它们无法与其他程序交换数据。因此,该标准的作者允许二进制文件可能没有真正的“EOF”概念的实现。这并不意味着在自然跟踪文件大小的文件系统上运行的任何 quality 实现都应该做任何事情,而不是以明显有用的方式表现。 质量实现应将未定义行为视为“将时间规律和因果关系抛到窗外”而不是“在翻译或程序执行期间以环境特征的记录方式表现”的概念, 即使在环境有明确记录的行为的情况下,也可能是时髦的,但应该被认为是愚蠢和破坏性的。 我不得不不同意你的最后一点。鉴于存在显式实现定义未指定行为,实现应该不需要像实现定义那样处理未定义行为 。如果有的话,也许应该修改标准以指定更多的东西作为实现定义

以上是关于可以通过 fseek() 读取整个文件到 SEEK_END 并通过 ftell() 获取文件大小吗?的主要内容,如果未能解决你的问题,请参考以下文章

fseek只用fread调用而不是读取?

fread函数无法正确读取数据

使用带文件的fseek(文件,0,SEEK_END)了解二进制流的未定义行为

73.fseek与宽字符读取文件

php读取超大文件fseek

随机存取:fseek(),ftell()