如何在不使用fseek和ftell的情况下获取ANSI C中的文件大小?

14

在寻找给定 FILE* 的文件大小的方法时,我找到了 这篇文章 警告不要使用它。相反,它似乎鼓励使用文件描述符和 fstat

然而,我印象中的是,fstatopen 和文件描述符总体上并不是那么可移植(经过一番搜索,我发现了一些关于这方面的信息,请参考此处)。

是否有一种方式可以在 ANSI C 中获取文件大小,同时保持与文中警告的一致性?


请注意,你链接的文章被认为是有害的。fseek/ftell(如果你使用POSIX,实际上是 fseeko/ftello,这样你就可以处理大文件)是确定文件大小的首选方式。基于 stat 的替代方法将无法确定某些非常规文件的大小(如块设备(磁盘分区等)),而这些文件确实具有明确定义的大小。 - R.. GitHub STOP HELPING ICE
虽然没有什么用,但是打开一个文件以追加模式工作:FILE* fp = fopen("teste.txt", "a"); size_t sz = ftell(fp); - Tiago Vieira
7个回答

15

在标准C中,fseek/ftell是唯一的选择。任何其他操作都至少部分依赖于程序运行所在的特定环境。不幸的是,正如你提供的文章所述,这种方式也存在问题。

我猜想你可以一直读取文件直到EOF,并沿途进行跟踪 - 例如使用fread()


我认为答案被downvote是因为C标准中的特定措辞,至少应该被提及:将文件位置指示器设置为文件结尾,例如fseek(file,0,SEEK_END),对于二进制流(由于可能存在尾随空字符)或具有状态相关编码的任何流而言都具有未定义行为,这些流不保证以初始换档状态结束。二进制流不必支持whence值为SEEK_END的fseek调用。 - Alexey Frunze
6
请注意,ISO C未定义二进制文件的结尾,但POSIX却定义了,并且所有实际上使用的C的后1980年实现都同意这一点。二进制文件具有确切的大小,您可以相对于结尾进行查找。 - R.. GitHub STOP HELPING ICE
1
但是,根据C语言标准,使用POSIX函数是未定义行为。在解决此问题时,没有对未定义行为的解决方案。使用SEEK_ENDfseek是未定义行为,并调用不在ISO C和程序中的函数也是未定义行为。解决这个问题以及大多数日常问题需要从自己的眼中移除ISO C的限制。 - Kaz
@Kaz:“使用 SEEK_END 的 fseek 是未定义行为” - 真的吗?我认为“将文件位置指示器设置为文件末尾,就像使用 fseek(file, 0, SEEK_END) 一样”是未定义行为。所以在 SEEK_END 前设置 N 字节的位置(fseek(file, -1, SEEK_END))- 根据标准,这种行为似乎是可以的。 - Agnius Vasiliauskas
@0x69 我会担心那些小于等于1字节的文件。但是这看起来值得阅读一些手册页。 - sehe
显示剩余2条评论

7
该文章声称fseek(stream, 0, SEEK_END)是未定义行为,引用了一个脱离上下文的脚注。
该脚注出现在处理宽向流的文本中,这些流是首次对宽字符执行操作的流。
这种未定义行为源于两个段落的结合。首先,§7.19.2/5 表明:
— 二进制宽向流具有分别归因于文本流和二进制流的文件定位限制。
对于文本流的文件定位限制(§7.19.9.2/4),则有以下规定:
对于文本流,offset 要么为零,要么为与同一文件相关联的流的 ftell 函数的早期成功调用返回的值,且 whence 必须为 SEEK_SET。
这使得 fseek(stream, 0, SEEK_END) 变成了针对宽向流的未定义行为。而对于字节流则没有 §7.19.2/5 这样的规定。
此外,当标准说:
二进制流不需要支持具有 whence 值 SEEK_END 的 fseek 调用。
时,这并不意味着这样做就是未定义行为。但如果流支持它,则可以使用。
显然,这是为了允许二进制文件拥有粗略的大小粒度,即大小是磁盘扇区数而不是字节数,并因此允许未指定的零出现在二进制文件的末尾。在这种情况下,不能有意义地支持 SEEK_END。其他示例包括管道或无限的文件,如 /dev/zero。然而,C 标准没有提供区分这些情况的方法,因此如果您想考虑这一点,则必须使用依赖于系统的调用。

1
最后一段不太对。ISO C允许二进制文件具有粗略的大小粒度,即大小可以是磁盘扇区数而不是字节数,并且因此允许未指定数量的零在二进制文件末尾神奇地出现。这就是为什么SEEK_END可能无法被“有意义地”支持的原因。然而,没有真实世界的实现会如此糟糕;此外,POSIX禁止这样做。 - R.. GitHub STOP HELPING ICE
@R.. 哦,谢谢。那确实很奇怪。那些末尾的空值会被 fread 读取吗? - R. Martinho Fernandes
1
该文章并未引用一个脱离上下文的脚注,而是引用了一个相关的脚注。文章中的基本论点是基于规范文本的。文章的作者将规范文本和未定义行为的概念从理性的背景中剥离出来,并没有意识到所提出的解决方案(使用平台特定的函数,不在C程序或标准库中定义)也是形式上的未定义行为。 - Kaz

3

使用fstat - 需要文件描述符 - 可以从FILE*fileno中获取 - 因此,大小与其他细节都在你的掌握之中。

即。

fstat(fileno(filePointer), &buf);

其中filePointerFILE *

buf则是

struct stat {
    dev_t     st_dev;     /* ID of device containing file */
    ino_t     st_ino;     /* inode number */
    mode_t    st_mode;    /* protection */
    nlink_t   st_nlink;   /* number of hard links */
    uid_t     st_uid;     /* user ID of owner */
    gid_t     st_gid;     /* group ID of owner */
    dev_t     st_rdev;    /* device ID (if special file) */
    off_t     st_size;    /* total size, in bytes */
    blksize_t st_blksize; /* blocksize for file system I/O */
    blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
    time_t    st_atime;   /* time of last access */
    time_t    st_mtime;   /* time of last modification */
    time_t    st_ctime;   /* time of last status change */
};

1
正如之前的帖子所指出的,操作系统不同 - 但是在Windows中也有类似的东西。等价于 fstat 的功能是可用的。 - Ed Heal
最好的选择是根据操作系统使其正常工作。 - Ed Heal
投赞成票,因为这是符合POSIX标准的。 - Guido
6
小心,威尔·罗宾逊!如果你在一个已经通过 FILE* 写入过内容的文件上使用 fstat(),由于尚未写入缓存数据,它可能会返回错误的文件大小。请注意避免此类情况。 - David Given
我做出了假设,认为这个人要么在开始的时候就考虑到这一点(正如主题帖中所示),要么使用刷新功能。 - Ed Heal
虽然您指出了一个常见的陷阱,但显然 stat 在那里不会报告“错误的大小”——它实际上是文件的大小(因为未缓冲的更改尚未被写入)。 - sehe

3
总结一下,你必须使用fseek/ftell,因为没有更好的选择(甚至是特定于实现的选择)。
根本问题在于文件以字节为单位的“大小”并不总是等于文件中数据的长度,并且在某些情况下,数据的长度不可用。
POSIX的一个例子是当您将数据写入设备时会发生什么;操作系统只知道设备的大小。一旦数据被写入并且(FILE*)关闭,就没有记录写入的数据长度。如果打开设备进行读取,则使用fseek/ftell方法将失败或者将给出整个设备的大小。
当ANSI-C委员会在1980年代末开会时,成员们记得一些操作系统根本没有存储文件中数据的长度;相反,他们存储了文件的磁盘块,并假设数据中的某些内容终止了该块。'text'流表示这一点。在这些文件上打开一个'binary'流不仅显示魔术终止符字节,而且显示在同一磁盘块中但从未写入的任何字节。
因此,C-90标准编写成可以使用fseek技巧;结果是一个合规程序,但结果可能不是您预期的。该程序在C-90定义中的行为不是“未定义”的,也不是“实现定义”的(因为在UN * X上它与文件有关)。它也不是“无效”的。相反,您会得到一个您不能完全依赖的数字,或者根据fseek的参数是-1和errno。
实际上,如果技巧成功,您将得到包括所有数据的数字,这可能是您想要的,如果技巧失败,则几乎肯定是别人的问题。

2
在不同操作系统中,提供了不同的API来实现此功能。例如,在Windows中我们有:
GetFileAttributes()
在MAC中,我们有:
[[[NSFileManager defaultManager] attributesOfItemAtPath:someFilePath error:nil] fileSize];
但是,使用fread和fseek这些原始方法只能实现文件大小的获取: 如何在C语言中获取文件的大小?

2

有时候无法避免编写特定于平台的代码,尤其是当您必须处理与平台相关的内容时。文件大小取决于文件系统,所以通常我会使用本地文件系统API来获取该信息而不是使用fseek / ftell操作。为了不使应用程序逻辑混杂具体平台细节并且使代码易于移植,我会创建自己的通用包装器。


-2

这篇文章存在一些逻辑问题。

它(正确地)指出了某些 C 函数的使用行为不符合 ISO C 的定义。但是,为了避免这种未定义的行为,文章提出了一个解决方案:用特定于平台的函数替换该使用方式。然而,根据 ISO C 的规定,使用特定于平台的函数也是未定义的。因此,这个建议并没有解决未定义行为的问题。

我手头的 1999 年标准中的引用证实了所谓的行为确实是未定义的:

二进制流不需要有意义地支持 SEEK_END 值的 fseek 调用。[ISO 9899:1999 7.19.9.2 第 3 段]

但是未定义的行为并不意味着“不好的行为”;它只是 ISO C 标准没有定义的行为。并非所有未定义的行为都相同。

一些未定义的行为是语言中可以提供有意义扩展的领域。平台通过定义行为来填补这个空白。

提供一个可工作的fseek,可以从SEEK_END进行搜索,这是一种替代未定义行为的扩展示例。可以确认给定平台是否支持从SEEK_END进行fseek,如果有,则可以使用它。

提供一个单独的函数,如lseek,也是替代未定义行为的扩展(调用不在ISO C中且未在C程序中定义的函数的未定义行为)。如果可用,则可以使用它。

请注意,那些具有类似POSIX lseek的功能的平台也很可能具有从SEEK_END工作的ISO C fseek。还要注意,在无法从二进制文件上的fseek进行SEEK_END搜索的平台上,可能的原因是这是不可能做到的(无法提供API来执行此操作,这就是为什么C库函数fseek无法支持它的原因)。

因此,如果fseek在给定平台上提供了所需的行为,则程序无需进行任何更改;将其更改为使用该平台的特殊功能是一种浪费。另一方面,如果fseek未提供行为,则很可能什么也没有提供。

请注意,即使包括程序中不存在的非标头文件也是未定义的行为。(通过省略行为的定义。)例如,如果以下内容出现在C程序中:

#include <unistd.h>

行为在此之后没有定义。[请参见下面的参考文献。] 预处理指令 #include 的行为是被定义的。但这会产生两种可能性:要么头文件 <unistd.h> 不存在,需要进行诊断;要么该头文件存在,但其内容并不为所知(就 ISO C 而言,库中没有记录过这样的头文件)。在这种情况下,包含指令会引入一段未知的代码块,并将其合并到转译单元中。无法定义未知代码块的行为。

#include <platform-specific-header.h> 是语言中一个逃生舱口,用于在特定平台上执行任何操作。

点形式:

  • 未定义行为本质上并非“不好的”,也不一定是安全漏洞(当然它可能是!例如与指针算术和解引用的未定义行为相关的缓冲区溢出)。
  • 仅出于避免未定义行为的目的而将一个未定义行为替换为另一个未定义行为是毫无意义的。
  • 未定义行为只是 ISO C 中用来表示超出 ISO C 定义范围的事物的特殊术语。它并不意味着“没有任何人在世界上定义过”并且不意味着某些东西是有缺陷的。
  • 为了制作大多数实际的、有用的程序,依赖一些未定义行为是必要的,因为许多扩展都是通过未定义行为提供的,包括平台特定的头文件和函数。
  • 可以通过来自 ISO C 外部的行为定义来取代未定义行为。例如,POSIX.1 (IEEE 1003.1) 系列标准定义了包括 <unistd.h> 的行为。一个未定义的 ISO C 程序可以是一个良定义的 POSIX C 程序。
  • 有些问题在 C 中无法解决而不依赖某种未定义行为。其中一个例子是一个想要从文件末尾向后定位这么多字节的程序。

  • 2
    天啊,不要再出现了... 这不是未定义行为。 - user1203803
    1
    我认为你混淆了“未定义行为”和“实现定义行为”。 - Etienne de Martel
    3
    实际上,我认为混淆是关于“未定义行为”适用于什么:编译器处理包含文件的行为非常明确定义。但明显生成的程序可能会有未定义的行为(甚至可能无效)。通常,“未定义行为”指的是编译器的操作/输出,而不是生成的程序的行为(尽管在同一时间理解它变得很困难)。 - sehe
    未定义行为是指在使用错误的程序结构或错误的数据时可能出现的行为,对此C++标准没有任何要求。当C++标准省略了任何明确定义行为的描述或将行为定义为不合法且无需诊断时,也可能会出现未定义行为。尽管有些程度上存在歧义,但未定义行为确实是不好的,因为您无法知道会发生什么。不知道程序将做什么是不好的,不是吗? - shawn1874
    @shawn1874 你的理解有误。在 ISO C++ 的上下文中,“未定义行为”意味着“不被 ISO C++ 定义”,而不是“根本没有人定义”。编译器提供了有用的、记录在案的扩展,这些扩展属于 ISO C++ 的未定义行为范畴,程序员可以利用它们。 - Kaz
    显示剩余3条评论

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