当前位置: 首页 > 工具软件 > fwrite > 使用案例 >

使用fread函数和fwrite函数进行读写数据的优化

范浩宕
2023-12-01

引言

        众所周知,在C语言里面,我们可以使用 scanf 函数读取数据,使用 printf 函数输出数据。由于这两个函数常常出现在C语言入门教材里面,它们常常受到广大学生的青睐。无论是各大OJ平台的题解中,还是用C编写一些简单的系统代码里面,这两个函数随处可见。

        然而,当一个程序的读写速度成为性能的瓶颈(现实中很少会出现),我们就不得不考虑其他更加高效的读写函数,比如 getchar 函数、putchar 函数、puts 函数,由于它们只针对字符或字符串进行读写,无需像 scanf 和 printf 那样对读取的数据进行转换或格式化,往往可以有效地提升程序读写速度。当然,它们今天并不是我们这篇文章的主角。现在,如果你还是嫌弃读写速度太慢的话,就应该考虑使用今天的主角—— fread 函数 fwrite 函数啦。

初探

        这两个函数躺在标准库 stdio.h 中,与很多其他函数相比,显得默默无闻。我们先看看它们的定义:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) 

  • ptr -- 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
  • size -- 这是要读取的每个元素的大小,以字节为单位。
  • nmemb -- 这是元素的个数,每个元素的大小为 size 字节。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

  • ptr -- 这是指向要被写入的元素数组的指针。
  • size -- 这是要被写入的每个元素的大小,以字节为单位。
  • nmemb -- 这是元素的个数,每个元素的大小为 size 字节。
  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。

也可以参考这两个链接:fread函数和 fwrite函数 

        上面的定义中 size_t 是一种由标准库定义的无符号整数类型,这两个函数的大致功能是,从(向)给定的文件(stream)读取(写入)一定量的数据,这些要读取(写入)的数据保存在 ptr 指针指向的内存中,数据总量由元素的大小 size 和元素个数 nmemb 确定。由此,fread 函数和 fwrite 函数的四个参数的功能便清楚了。

深入

        你可能会问,这两个函数是从文件里面读取数据,平时我们写程序习惯于从黑窗口手动输入,它们如何派得上用场?这时就要感谢标准库为我们做的一些人性化工作啦。stdio.h 库中定义了 标准输入流 stdin标准输出流 stdout,我们把fread函数的第四个参数指定为 stdin,它就可以从命令行窗口读取我们输入的数据,把 fwrite 函数的第四个参数指定为stdout,即可完成向命令行窗口的输出。

        说到这里,不妨扯几句题外话,我们常见的 scanf 函数和 printf 函数无非就是 fscanf 函数和 fprintf 函数将文件指针参数分别指定为 stdin 、stdout的一种特例,举个例子,见下表:

要求习惯写法另类写法
读取一个整数scanf("%d", &a);fscanf(stdin ,"%d", &a);
输出一个整数printf("%d", a);fprintf(stdout,"%d", a);

上表里面习惯写法和另类写法本质上是等价的,而且有的标准库就是先实现一个 fscanf 方法,把其中的文件指针指向 stdin 就得到了我们常用的scanf 函数(printf函数同)。

        言归正传,我们用 fread 和 fwrite 可以实现更快速的读取,它的究竟是如何实现的?实现的原理又是什么呢?

实现及其原理

        不妨先给出这种快速读写的实现代码,如下所示。

#include <stdio.h>
#include <ctype.h>

//fread() 读取 
inline char getc(){
	static char buf[100000], *p1 = buf, *p2 = buf;
	return p1 == p2 && (p2 = (p1=buf) + fread(buf,1,100000,stdin),p1 == p2) ?
				EOF : *p1++;
}


char buf[100000], *pp = buf;

inline void putc(char c){
	if(pp - buf == 100000)	fwrite(buf,1,100000,stdout),pp = buf;
	*pp++ = c;
}

//buf未满,在结尾要调用fsh函数刷新缓冲区,确保结果输出 
inline void fsh(){
	fwrite(buf,1,pp-buf,stdout);
	pp = buf;
}

int main(){
	char c = getc();
	while(c != EOF)	putc(c), c = getc();
	fsh();
	return 0;
}

        观察上面的代码,我们会发现有两个同名的 buf 字符数组,一个作为函数内部的静态数组,一个作为全局数组,它们是不一样的。这也正是这种读写方法的快速的原因所在。这两个 buf 数组其实起到了输入缓冲区和和输出缓冲区的作用。

        计算机组成原理的知识告诉我们,计算机中不同的部件存取速度不同,CPU速度 > 内存速度 > I/O设备速度,其中以I/O设备存取速度最慢。我们通过标准输入输出进行读写,实际上就是通过I/O设备进行的读写,而访问数组里某个元素属于对内存的读写,如果可以减少I/O设备读写的次数,我们无疑可以为程序节省许多时间。

        我们的快读代码中,fread 和 fwrite 的作用正在于此:先不管三七二十一,利用 fread 从标准输入流一口气读上100000个字符(如果不足100000,就读到文件结束),存入buf数组里,这样后面要获取数据时只需进行内存访问即可,而不必多次进行I/O读入,输出数据时类似,先把待输出的数据放在输出缓冲区buf(注意,它和那个输入缓冲区buf不一样)中,待缓冲区满,一次性把内容全部输出。最终这么运行下来,时间就节省下来了,这就是缓冲区的妙用!

参考资料

[1]        王道论坛 . 2023年计算机组成原理考研复习指导[M] . 电子工业出版社

 类似资料: