如何在没有 fseek 和 ftell 的情况下在 ANSI C 中获取文件大小?
Posted
技术标签:
【中文标题】如何在没有 fseek 和 ftell 的情况下在 ANSI C 中获取文件大小?【英文标题】:How to get file size in ANSI C without fseek and ftell? 【发布时间】:2012-03-22 21:25:39 【问题描述】:在寻找给定 FILE*
的文件大小的方法时,我遇到了 this article 建议不要这样做。相反,它似乎鼓励使用文件描述符和fstat
。
但我的印象是,fstat
、open
和文件描述符通常不那么可移植(经过一番搜索,我找到了这个 effect 的内容)。
有没有办法在 ANSI C 中获取文件的大小,同时与文章中的警告保持一致?
【问题讨论】:
请注意,您链接到的文章被认为是有害的。fseek
/ftell
(实际上是fseeko
/ftello
,如果你有POSIX,那么你可以处理大文件)是确定文件大小的首选方法。基于stat
的替代方案将无法确定一些确实具有明确定义大小的非常规文件的大小,例如块设备(磁盘分区等)。
没用,但是...以附加模式打开文件有效:FILE* fp = fopen("teste.txt", "a"); size_t sz = ftell(fp);
【参考方案1】:
在标准 C 中,fseek
/ftell
舞蹈几乎是城里唯一的游戏。您要做的任何其他事情至少在某种程度上取决于您的程序运行的特定环境。不幸的是,如您链接的文章中所述,舞蹈也有其问题。
我猜您总是可以在 EOF 之前从文件中读取所有内容并一路跟踪 - 例如 fread()
。
【讨论】:
我认为由于 C 标准中的特定措辞,答案被否决了,至少应该提到:Setting the file position indicator to end-of-file, as with fseek(file, 0, SEEK_END), has undefined behavior for a binary stream (because of possible trailing null characters) or for any stream with state-dependent encoding that does not assuredly end in the initial shift state.
和 A binary stream need not meaningfully support fseek calls with a whence value of SEEK_END.
不幸的是,它也没有提供任何其他选项。
也许没必要。您可以fread()
或fgetc()
直到EOF
,这不是很快,但应该可以工作并且更便携。
请注意,虽然 ISO C 没有定义二进制文件的结尾,但 POSIX 定义了,并且所有现实世界的 1980 年后的 C 实现都同意这个问题。二进制文件有一个精确的大小,你可以相对于结尾来寻找。
但是根据C,使用POSIX函数是未定义的行为。在解决这个问题时没有解决未定义的行为。 fseek
使用 SEEK_END
是未定义的行为,调用不在 ISO C 和程序中的函数是未定义的行为。解决这个问题以及大多数其他日常问题,需要从眼睛上取下 ISO C 眼罩。【参考方案2】:
文章通过引用一个断章取义的脚注,声称fseek(stream, 0, SEEK_END)
是未定义的行为。
脚注出现在处理面向宽的流的文本中,这些流是对它们执行的第一个操作是对宽字符的操作。
这种未定义的行为源于两个段落的组合。首先 §7.19.2/5 说:
— 二进制宽向流具有归因于文本流和二进制流的文件定位限制。
使用文本流(第 7.19.9.2/4 节)进行文件定位的限制是:
对于文本流,
offset
应为零,或offset
应为先前成功调用与同一文件关联的流上的ftell
函数返回的值,whence
应为SEEK_SET
.
这使得fseek(stream, 0, SEEK_END)
的行为未定义对于面向宽的流。对于面向字节的流,没有像 §7.19.2/5 这样的规则。
此外,当标准说:
二进制流不需要有意义地支持
whence
值为SEEK_END
的fseek
调用。
这并不意味着这样做是未定义的行为。但如果流支持它,就可以了。
显然,这允许二进制文件具有粗粒度粒度,即大小为磁盘扇区数而不是字节数,因此允许在末尾神奇地出现未指定数量的零的二进制文件。在这种情况下,SEEK_END
无法得到有意义的支持。其他示例包括管道或无限文件,如/dev/zero
。但是,C 标准没有提供区分这些情况的方法,因此如果您想考虑这一点,您将不得不使用与系统相关的调用。
【讨论】:
最后一段不太对。 ISO C 允许二进制文件具有课程大小粒度,即大小为磁盘扇区数而不是字节数,因此允许在二进制文件末尾神奇地出现未指定数量的零。这就是SEEK_END
可能不受“有意义”支持的原因。尽管如此,现实世界中的任何实现都不会如此糟糕。此外,POSIX 禁止它。
@R.. 哦,谢谢。那确实会很奇怪。最后的那些空值会被fread
读取吗?
文章没有引用断章取义的脚注;它引用了一个相关的脚注。文章中的基本主张基于规范性文本。这篇文章的作者将规范文本和未定义行为的概念排除在合理的背景之外,并没有意识到所提出的解决方案(使用平台特定函数,未在 C 程序或标准库中定义)也是正式的, 未定义的行为。【参考方案3】:
使用fstat - 需要文件描述符 - 可以从fileno 从FILE*
获取 - 因此大小以及其他细节都在您的掌握之中。
即
fstat(fileno(filePointer), &buf);
其中filePointer
是FILE *
和
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 */
;
【讨论】:
正如之前的海报所指出的,操作系统不同 - 但 Windows 可以使用相同的东西。等效于fstat
可用。
猜猜最好的选择是让它根据操作系统工作。
投了赞成票,因为它是 POSIX 的标准。
危险,威尔罗宾逊!如果您在之前通过FILE*
写入内容的打开文件上使用fstat()
,它很可能会返回错误的大小,因为尚未写入未缓冲的数据。
我假设这个人要么在开始时考虑到这一点(如 OP 中所暗示的那样),要么使用同花顺。【参考方案4】:
执行摘要是您必须使用 fseek/ftell,因为没有更好的替代方案(甚至是特定于实现的方案)。
根本问题是文件的“大小”(以字节为单位)并不总是与文件中数据的长度相同,并且在某些情况下,数据的长度不可用。
一个 POSIX 示例是当您将数据写入设备时发生的情况;操作系统只知道设备的大小。一旦数据被写入并且 (FILE*) 关闭,就没有写入数据长度的记录。如果打开设备进行读取,fseek/ftell 方法要么失败,要么给你整个设备的大小。
当 ANSI-C 委员会在 1980 年代末召开会议时,成员们记得许多操作系统根本没有将数据的长度存储在文件中。相反,他们存储了文件的磁盘块,并假设数据中的某些内容终止了它。 “文本”流代表了这一点。在这些文件上打开“二进制”流不仅会显示魔法终止字节,还会显示超出它但从未写入但恰好位于同一磁盘块中的任何字节。
因此编写了 C-90 标准,以便使用 fseek 技巧有效;结果是一个符合标准的程序,但结果可能不是您所期望的。该程序的行为在 C-90 定义中不是“未定义的”,也不是“实现定义的”(因为在 UN*X 上它随文件而变化)。它也不是“无效的”。相反,您会得到一个不能完全依赖的数字,或者可能取决于 fseek、-1 和 errno 的参数。
在实践中,如果技巧成功,您会得到一个至少包含所有数据的数字,这可能是您想要的,如果技巧失败,几乎可以肯定是其他人的错。
约翰·鲍勒
【讨论】:
【参考方案5】:不同的操作系统为此提供不同的 api。例如在 windows 中我们有:
GetFileAttributes()
在 MAC 中我们有:
[[[NSFileManager defaultManager] attributesOfItemAtPath:someFilePath error:nil] fileSize];
但是 raw 方法只能通过 fread 和 fseek 实现: How can I get a file's size in C?
【讨论】:
【参考方案6】:您不能总是避免编写特定于平台的代码,尤其是当您必须处理平台功能的事情时。文件大小是文件系统的一个函数,因此通常我会使用本机文件系统 API 通过 fseek/ftell 舞蹈来获取该信息。我会围绕它创建自己的通用包装器,以免使用特定于平台的细节污染应用程序逻辑,并使代码更容易移植。
【讨论】:
【参考方案7】:文章逻辑有点问题。
它(正确地)识别出 C 函数的某种用法具有 ISO C 未定义的行为。但是,为了避免这种未定义的行为,本文提出了一个解决方案:用特定于平台的函数替换该用法。不幸的是,根据 ISO C,平台特定函数的使用也是未定义的。因此,建议并没有解决未定义行为的问题。
我的 1999 年标准副本中的引述证实了所谓的行为确实是未定义的:
二进制流不需要有意义地支持 wherece 值为 SEEK_END 的 fseek 调用。 [ISO 9899:1999 7.19.9.2 第 3 段]
但未定义的行为并不意味着“不良行为”;这只是 ISO C 标准没有给出定义的简单行为。并非所有未定义的行为都是相同的。
一些未定义的行为是语言中可以提供有意义扩展的区域。该平台通过定义行为来填补空白。
提供一个可以从SEEK_END
寻找的工作fseek
是一个扩展来代替未定义行为的示例。可以从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 指令会引入一段未知的代码,并将其合并到翻译单元中。无法定义未知代码块的行为。
#include <platform-specific-header.h>
是语言中的逃生舱之一,可以在给定平台上做任何事情。
点形式:
-
未定义的行为本质上不是“坏的”,也不是本质上的安全漏洞(尽管它当然可以是!例如,与指针算术和取消引用领域中的未定义行为相关的缓冲区溢出。)
仅出于避免未定义行为的目的而将一种未定义行为替换为另一种行为是没有意义的。
未定义的行为只是 ISO C 中使用的一个特殊术语,用于表示 ISO C 定义范围之外的事物。这并不意味着“世界上没有任何人定义过”,也不意味着有缺陷。
依赖于一些未定义的行为对于制作大多数实际有用的程序是必要的,因为许多扩展是通过未定义的行为提供的,包括特定于平台的标头和函数。
未定义的行为可以被来自 ISO C 外部的行为定义所取代。例如,POSIX.1 (IEEE 1003.1) 系列标准定义了包含
<unistd.h>
的行为。未定义的 ISO C 程序可以是定义良好的 POSIX C 程序。
如果不依赖某种未定义的行为,C 中的某些问题无法解决。这方面的一个例子是一个程序想要从文件末尾向后查找这么多字节。
参考资料:
Dan Pop 在 comp.std.c 中,2002 年 12 月:http://groups.google.com/group/comp.std.c/msg/534ab15a7bc4e27e?dmode=source Chris Torek,comp.std.c,关于非标准函数是未定义行为的主题,2002 年 2 月:http://groups.google.com/group/comp.lang.c/msg/2fddb081336543f1?dmode=source Chris Engebretson,comp.lang.c,1997 年 4 月:http://groups.google.com/group/comp.lang.c/msg/3a3812dbcf31de24?dmode=source Ben Pfaff,comp.lang.c,1998 年 12 月 [引用包含非标准标头的不确定性的开玩笑回答]:http://groups.google.com/group/comp.lang.c/msg/73b26e6892a1ba4f?dmode=source Lawrence Kirby,comp.lang.c,1998 年 9 月 [解释非标准标头的影响]:http://groups.google.com/group/comp.lang.c/msg/c85a519fc63bd388?dmode=source Christian Bau,comp.lang.c,1997 年 9 月 [解释#include <pascal.h>
的未定义行为如何引入用于链接的 pascal 关键字。] http://groups.google.com/group/comp.lang.c/msg/e2762cfa9888d5c6?dmode=source
【讨论】:
天哪,不会再... 这不是未定义的行为。 我认为你混合了“未定义的行为”和“实现定义的行为”。 @Etienne de Martel, for the second time. 真的,我认为混淆是关于“未定义的行为”适用于:编译器的行为对于处理包括非常明确。生成的程序显然可能具有未定义的行为(见鬼,它甚至可能是格式错误的)。通常“未定义的行为”是指编译器的操作/输出。不是生成的程序的行为(尽管这当然很难同时推理) 不,“未定义的行为”只是指编程语言标准说它具有“未定义的行为”或没有提供行为定义的任何情况。这并不意味着“未由任何系统或供应商定义”。这意味着不是标准定义的。编译器的行为完全不是标准定义的! C 标准仅部分定义了处理#include <unistd.h>
时会发生什么。不足以真正定义后果。以上是关于如何在没有 fseek 和 ftell 的情况下在 ANSI C 中获取文件大小?的主要内容,如果未能解决你的问题,请参考以下文章