fread/fwrite为什么需要将size和count作为参数?

112

我们在工作中讨论了为什么fread()fwrite()需要按成员大小和数量读取/写入,并返回读取/写入的成员数,而不是只需一个缓冲区和大小。我们能想到唯一的用途是,如果您想要读取/写入一个数组,其中的结构体并非平台对齐方式刚好整除,因此已经填充,但这种情况不能如此常见以至于需要这种设计选择。

来自fread(3):

函数fread()从指向流的指针读取nmemb个大小为size字节的数据元素,并将它们存储在由ptr给定的位置。

函数fwrite()将nmemb个大小为size字节的数据元素从ptr给定的位置写入到指向流的指针,

fread()和fwrite()返回成功读取或写入的项目数(即不是字符数)。 如果发生错误或到达文件尾,则返回值为短项计数(或零)。


13
嘿,这是个好问题。我一直对它感到好奇。 - Johannes Schaub - litb
1
请查看此线程:https://dev59.com/kmoy5IYBdhLWcg3wZdCr - Franken
7个回答

86

fread(buf, 1000, 1, stream)fread(buf, 1, 1000, stream)的区别在于,第一种情况下您只会得到一个大小为1000字节的数据块或者什么都没有,如果文件比1000字节小;而在第二种情况下,您将获得文件中所有小于或等于1000字节的内容。


4
虽然这是正确的,但这只是故事的一小部分。更好的办法是对比读取某些内容,例如一个 int 值数组或一个结构体数组。 - Jonathan Leffler
4
如果完成了理由的说明,这将是一个很好的答案。 - Matt Joiner

24

它是基于如何实现fread的。

Single UNIX规范说:

对于每个对象,都应该调用size个fgetc()函数,并将结果按读取顺序存储在一个与对象完全重叠的unsigned char数组中。

fgetc也有这个注释:

由于fgetc()操作的是字节,因此读取由多个字节(或“多字节字符”)组成的字符可能需要多次调用fgetc()。

当然,这早于像UTF-8这样的复杂可变字节字符编码。

SUS指出,这实际上是来自ISO C文档。


18

以下只是纯属猜测,然而在过去的某个时期(现在仍有一些),许多文件系统并不是简单的硬盘字节流。

许多文件系统基于记录,因此为了高效地满足这样的文件系统,您需要指定项目的数量(“记录”),允许fwrite / fread将存储作为记录而不仅仅是字节流进行操作。


1
我很高兴有人提出这个问题。我曾经在文件系统规范、FTP以及记录/页面等阻塞概念方面进行了大量工作,尽管现在没有人再使用这些规范的那些部分,但它们仍然得到了坚定的支持。 - Matt Joiner

10

我来修复那些函数:

size_t fread_buf( void* ptr, size_t size, FILE* stream)
{
    return fread( ptr, 1, size, stream);
}


size_t fwrite_buf( void const* ptr, size_t size, FILE* stream)
{
    return fwrite( ptr, 1, size, stream);
}

关于fread()/fwrite()参数的合理性,我早已失去了K&R的副本,所以只能猜测。我认为一个可能的答案是,Kernighan和Ritchie可能认为执行二进制I/O最自然的方式是在对象数组上进行。此外,他们可能认为,在某些体系结构上,块I/O会更快/更容易实现或其他一些原因。
尽管C标准规定fread()fwrite()基于fgetc()fputc()实现,但请记住,标准诞生的时间远远晚于K&R定义C语言之前,并且标准中规定的事情可能不在最初设计者的想法中。甚至有可能K&R的"The C Programming Language"中说的事情与最初设计语言时不同。
最后,P.J. Plauger在"The Standard C Library"中对fread()的看法如下:
“如果第二个参数size大于1,则无法确定函数是否还读取了比报告的多size-1个字符。一般来说,最好将函数调用为fread(buf,1,size * n,stream);而不是fread(buf,size,n,stream);
基本上,他在说fread()的接口有缺陷。对于fwrite(),他指出“写错误通常很少发生,因此这不是一个主要缺点”-这是我不同意的说法。

21
其实我经常喜欢用另一种方式来做:fread(buf, size*n, 1, stream);如果读取不完整是错误情况的话,让fread返回0或1比返回实际读取的字节数更简单。这样你就可以像这样做:if (!fread(...))而不是必须将结果与所需的字节数进行比较(这需要额外的C代码和额外的机器码)。 - R.. GitHub STOP HELPING ICE
1
@R.. 只需确保在!fread(...)之外还检查size * count!= 0。 如果size * count == 0,则在成功读取(零字节)时会获得零返回值,feof()和ferror()不会设置,并且errno将是一些荒谬的东西,如ENOENT,或更糟糕,像EAGAIN这样具有误导性(可能会导致关键性错误) - 非常令人困惑,特别是因为基本上没有文档会向您大喊此陷阱。 - Pegasus Epsilon

3

可能这与文件I/O的实现方式有关。(在过去)分块写入/读取文件可能比一次性写入更快。


并非如此。C语言的fwrite规范指出它会重复调用fputc:http://www.opengroup.org/onlinepubs/009695399/functions/fwrite.html - Powerlord

1
拥有大小和数量的独立参数,在能够避免读取任何部分记录的实现中可能是有优势的。如果使用像管道这样的单字节读取,即使使用固定格式数据,也必须考虑记录可能被分成两个读取的可能性。如果可以请求例如非阻塞读取每个记录长度为10字节的40个记录,当有293字节可用时,并让系统返回290字节(29个完整记录),同时保留3字节准备下次读取,那将更加方便。我不知道fread的实现在多大程度上可以处理这样的语义,但它们肯定在能够承诺支持它们的实现中非常方便。

@PegasusEpsilon:例如,如果一个程序执行fread(buffer, 10000, 2, stdin),并且用户在输入了18,000字节后键入换行符-ctrl-D,那么如果该函数能够返回前10,000字节,同时保留剩余的8,000字节以供未来更小的读取请求使用,那将是很好的。但是是否有任何实现可以做到这一点?这8,000字节将存储在哪里,以便于未来的请求? - supercat
刚测试了一下,发现fread()在这方面的操作方式并不是我认为最方便的方式,但是在确定读取短记录后将字节塞回读取缓冲区可能比我们从标准库函数中期望的要多一些。fread()会读取部分记录并将它们推入缓冲区,但返回值将指定已经读取了多少完整记录,并且不会告诉你任何关于从stdin中读取的短记录的信息(这对我来说相当恼人)。 - Pegasus Epsilon
最好的方法可能是在fread()之前用null填充读取缓冲区,并在fread()返回后检查记录以查看是否存在非空字节。如果您要使用大于1的size,这并不能帮助您,但如果您将使用它,那么...顺便说一下,还可以对流应用ioctl或其他无聊操作以使其行为不同,但我没有深入研究过。 - Pegasus Epsilon
输入/输出流已经远远超出了文件描述符的抽象范畴,试图根据架构底层文件描述符的支持来定义它们的行为似乎有些愚蠢。更不用说read()和fread()都可以从库维护的读取缓冲区中读取,允许你想要的所有缓冲区戏法。但是关于C语言的一些设计,我持有异议。现在改变它们已经太晚了。 - Pegasus Epsilon
重点是,规范就是规范,在许多情况下必须做出选择:无论在特定平台上的难度如何,都必须在所有地方支持此功能,或者不支持该功能,因为在某些地方可能会很复杂。部分读取显然属于后者,而puts()显然属于前者,这个选择留给某个人去决定,他可能会或可能不会使用逻辑(公平地说,puts()比我的假想fread()更容易实现,但作为它目前的状态,它也比fread()更容易...)。 - Pegasus Epsilon
显示剩余6条评论

-2

我认为这是因为C语言缺乏函数重载。如果有的话,大小将是多余的。但在C语言中,您无法确定数组元素的大小,必须指定一个。

考虑一下这个例子:

int intArray[10];
fwrite(intArray, sizeof(int), 10, fd);

如果fwrite接受字节数,你可以这样写:
int intArray[10];
fwrite(intArray, sizeof(int)*10, fd);

但这只是低效的。你将拥有sizeof(int)次更多的系统调用。

另一个需要考虑的问题是,通常您不希望将数组元素的一部分写入文件。您要么要整个整数,要么什么都没有。fwrite返回成功写入的元素数量。因此,如果您发现只写入了元素的2个低字节,那么您会怎么做?

在某些系统上(由于对齐),您无法访问整数的一个字节而不创建副本和移位。


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