使用C语言中的fgets和fgetc来读取一行的区别

13

我需要读取一行文本(以换行符作为终止符),但不能假设其长度。因此,我现在面临两种可能性:

  • 使用fgets函数,并每次检查最后一个字符是否是换行符并将其连续附加到缓冲区中
  • 使用fgetc函数逐个读取每个字符,不时使用realloc重新分配缓冲区大小

直觉告诉我fgetc变体可能会更慢,但是我不知道fgets如何在不检查每个字符的情况下完成此操作(另外我的直觉并不总是那么好)。由于行很长,因此性能很重要。

我想知道每种方法的优缺点。谢谢。

5个回答

4
我建议使用fgets()和动态内存分配相结合的方式,或者您可以研究一下在POSIX 2008标准中提供的接口getline(),它在较新的Linux机器上可用并为您处理内存分配。您需要跟踪缓冲区长度以及其地址 - 因此,您甚至可以创建一个结构来处理这些信息。
虽然fgetc()也可以工作,但稍微麻烦一些 - 但仅仅是稍微麻烦一些。在底层,它使用与fgets()相同的机制。当直接调用fgetc()时,内部可能能够利用更快速的操作 - 类似于strchr() - 这些操作在调用fgetc()时不可用。

使用fgets实现getline函数的一个限制是无法同时处理空字节和文件不以换行符结尾的情况。如果fgets遇到EOF条件并返回时没有换行符,您只能假设字符串在第一个空字节处结束。(在其他情况下,您可以使用strchr(buf,'\n')查找读取停止的位置 - 或者如果没有'\n',则需要realloc。) - mk12
如果文件包含空字节,则它不是文本文件。(它可能是宽字符文件,但是这时你需要使用宽字符I/O函数来读取它。)并且fgets()函数不适用于处理包含空字节的文件——恰恰因为它不能可靠地指示已读取多少个字节。如果您的数据文件包含空字节,则(可能)不应使用fgets()函数来读取它。 - Jonathan Leffler
“返回值”部分似乎表明它可能是一个有用的东西。那就是我得到这个想法的地方,尽管我认为我同意你的观点。现在我想想,也许只是因为当使用除 '\n' 以外的定界符时,它可能会有用才被提到。 - mk12

2
你的环境是否提供getline(3)函数?如果是,我建议你使用它。我看到的最大优点是它可以自行分配缓冲区(如果需要),并且如果传入的缓冲区太小,则会对其进行realloc()操作。(因此,这意味着你需要传入从malloc()获取的内容。)这消除了使用fgets/fgetc时的一些痛苦,并且你可以希望实现它的C库编写者已经考虑到了效率问题。奖励:Linux上的man页面有一个很好的例子展示如何高效地使用它。

很遗憾(非常抱歉我在问题中没有提到这一点),我需要使用标准的东西:-( getline函数听起来确实很有吸引力。 - nc3b
1
好的,这是标准的(根据某种标准)。请参阅 The Open Group Base Specifications Issue 7,也称为“IEEE Std 1003.1™-2008”或“POSIX C 2008”。但是标准并不等同于普及,遗憾的是。我感同身受。getline 很性感 :-) - Mat
getline() 的功能很好;但是 getline() 这个名称对用户命名空间来说是一个可怕的侵入,抢占了更广泛使用的函数名称之一(例如,参见 K&R 1 和 2),并提供了各种不同的接口。使用那个名称是一个令人震惊的决定;提供该功能是一个极好的决定。唯一令人惊讶的是忽略了处理 CRLF 行尾的能力;相关的 getdelim() 函数可以处理 CR 或 LF 或 NUL 行尾,但无法处理 CRLF 行尾。 - Jonathan Leffler

2
如果性能对您很重要,通常应该调用getc而不是fgetc。标准试图将其实现为宏以避免函数调用开销。
除此之外,处理的主要问题可能是分配缓冲区的策略。大多数人使用固定增量(例如,当我们用完空间时,分配另外128字节)。我建议改用一个常数因子,因此如果用完空间,则分配一个大小为前一次的1.5倍的缓冲区。
特别是当getc被实现为宏时,getcfgets之间的差异通常非常小,因此您最好集中精力处理其他问题。

0

如果您可以设置最大行长度,即使很大,那么一个fgets就可以解决问题。如果不能,则多个fgets调用仍然比多个fgetc调用更快,因为后者的开销更大。

不过,更好的答案是,在必要之前,不值得担心性能差异。如果fgetc足够快,那又有什么关系呢?


请注意,getc通常被实现为宏,因此比fgetc更快速,并且只要你小心(参数不能是表达式),就应该使用它。 - mk12

0

我会分配一个大缓冲区,然后使用fgets进行读取,检查、重新分配并重复操作,直到读取到行尾。

每次读取(无论是通过fgetc还是fgets)都会进行系统调用,这需要时间,你希望尽量减少这种情况发生的次数,因此调用fgets的次数较少并在内存中迭代会更快。

如果你从文件中读取,另一种选择是使用mmap()映射文件。


我必须在系统调用部分与您持相反意见:stdio库会进行缓冲,因此我认为并不是每个函数调用都会被转换为系统调用。我可能是错的。 - nc3b
这是正确的,但使用fgets可以更精细地控制。如果他对每行的平均长度有一些了解,他可以优化缓冲区长度,而不是fgetc会缓冲但完全无法理解理想的缓冲区长度。 - Jesse Cohen

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