C生万物 | C语言文件操作指南汇总【内附文件外排序源码】

举报
烽起黎明 发表于 2023/02/26 12:44:07 2023/02/26
【摘要】 从0到1教你学会C语言文件操作。附有文件外排序实战训练加成,对文件操作更上一层楼

一、为什么使用文件?

我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受

所以就想到了通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在【磁盘文件】、存放到【数据库】等方式

二、什么是文件?

==磁盘上的文件是文件==

但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)

1、程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

  • 【程序文件】一般指的是我们创建工程时所编写的代码,也就想下面这个【test.c】一样

在这里插入图片描述

2、数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

  • 【数据文件】一般指通过程序去操纵的那个文件

在这里插入图片描述

  • 就想上面的这个【test.txt】就是一个数据文件,通过【test.exe】运行起来时,内存中有有了数据,此时我们可以将数据写到这个【test.txt】中,自然也可以从这个文件中读取数据到内存中

3、文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用

  • 文件名包含3部分:文件路径+文件名主干+文件后缀
  • [x] 例如:c:\code\test.txt
  • 为了方便起见,文件标识常被称为文件名。

三、文件的打开和关闭

1、文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”

  • 每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名【FILE】

例如,VS2019编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
       };
typedef struct _iobuf FILE;
FILE* pf;//文件指针变量
  • 不同的C编译器的FILE类型包含的内容不完全相同,但是==大同小异==。每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节
  • 一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。我们来看看如何创建一个FILE*的指针变量
FILE* pf;//文件指针变量
  • 定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

在这里插入图片描述

2、文件的打开和关闭【⭐】

接下去来讲讲有关文件的打开和关闭,如果说上面都是理论基础,那么这一块的话就要涉及到代码了,所以竖起耳朵:ear:哦

首先举两个栗子🌰C语言中文件的打开、操作、关闭的流程基本就是下面这样,可做参考
在这里插入图片描述
注:文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

  • ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。显示如何打开和关闭文件的个格式
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );

  • 下面是文件的一些打开方式,有很多的操作,大家挑重点记就行

==注:a即append(追加);b即binary(二进制)==

文件使用方式 含义 如果指定文件不存在
【重点】“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
【重点】“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
【重点】“a”(追加) 向文本文件尾添加数据 建立一个新的文件
rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“a+”(读写) 打开一个二进制文件,在文件尾进行读写 建立一个新的文件

例:

int main(void)
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf)
	{
		perror("fail fopen");
		return 1;
	}

	//写文件
	
	//关闭文件
	fclose(pf);
	pf = NULL;		//防止野指针
	return 0;
}

四、文件的顺序读写【重点掌握】

接下去我们来聊聊有关文件的顺序读写操作,首先要说的就是一些重要的库函数

1、8个重要的库函数

下面的8个库函数都很重要,大家最好都要记住,而且对于它们的用法也要熟知

功能 函数名 适用于
字符输入函数【读】 fgetc 所有输入流
字符输出函数【写】 fputc 所有输出流
文本行输入函数【读】 fgets 所有输入流
文本行输出函数【写】 fgets 所有输入流
格式化输入函数【读】 fscanf 所有输入流
格式化输出函数【写】 fprintf 所有输入流
二进制输入【读】 fread 文件
二进制输入【写】 fwrite 文件

对于上面的这些函数的使用最关键的一点就是:【读】对应的输入流,【写】对应的输出流

  • 在初识C语言时,我们学习了【scanf】和【printf】,只要了如何从键盘读取数据,然后将数据显示在屏幕上

在这里插入图片描述

  • 现在我们可以从键盘、屏幕过渡到文件,也可以从文件读、写数据

接下去就让我们来一一认识一下他们吧

1.1 单字符输入输出【fputc和fgetc】

这连个比较简单,我一说你就能懂

  • 首先我们去cplusplus里面找到这两个函数的描述

在这里插入图片描述

好,有了一个基本的了解后,我们就到VS2019中去实操一下

  • 首先是写文件,我们往【test.txt】中写一个字符a进去
//写文件
fputc('a', pf);

在这里插入图片描述

  • 既然能写一个,那我们多写几个试试

在这里插入图片描述

  • 那我们能不能将26个字母都写进去呢?当然是可以的,不过不是这么一句一句写,要用循环来写
for (int i = 0; i < 26; ++i)
{
	fputc('a' + i, pf);
}

在这里插入图片描述


  • 可以写数据了,那能不能将我们写进去的内容再读出来呢,这就要用到 fgetc() 了,而且在打开文件的时候要以【读】也就是【r】的形式打开
  • 既然是读取数据,那我们就要去接收读到的这个数据,刚才看到这个库函数的返回值是【int】,所以我们就这么去接收
int ch = fgetc(pf);
printf("读出来的字符为:%c\n", ch);

在这里插入图片描述

  • 能读一个,那也能读多个,我们多读几个试试
int ch = fgetc(pf);
printf("读出来的字符为:%c\n", ch);
ch = fgetc(pf);
printf("读出来的字符为:%c\n", ch);
ch = fgetc(pf);
printf("读出来的字符为:%c\n", ch);

在这里插入图片描述

  • 然后我们再把这26个字母都读出来试试
for (int i = 0; i < 26; ++i)
{
	printf("%c ", fgetc(pf));
}
printf("\n");

在这里插入图片描述

  • 但是呢,我们平常在读取文件中内容的时候,并不知道里面有什么东西,有多少东西,因此应该写一个通过的程序,才能适应更多的情况
  • 我们再仔细看看fgetc的简述。可以看到当它读到文件末尾的时候便会返回EOF,即End Of File(文件结束)

在这里插入图片描述

  • 此时我们就可以将代码写成这样。将for循环改为while循环
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
	printf("%c ", ch);
}
printf("\n");

可以看到,一样是可以显示出来的

在这里插入图片描述

1.2 文本行输入输出【fputs和fgets】

接下来说说有关文本行的输入输出

  • 首先来了解一下这个两个函数

在这里插入图片描述

  • 然后我们像向文件中写入一个字符串试试

在这里插入图片描述

  • 接下去多写几行试试

在这里插入图片描述


  • 可以写东西进去了,接下去一样,将我们写的东西读出来试试
  • 可以看到,我这里初始化一个数组开始读取,在读取结束之后DeBug和显示窗口都可以看到只有四个字符,并没有5个,这是为什么呢?似乎是读到了一个换行符

在这里插入图片描述

  • 我们再仔细地观察一个这个函数

在这里插入图片描述

  • 看了一些官方文档的描述,应该清楚为什么会只有四个了吧,
  • [x] 若是最大字符数num < 本行的字符数,那么就会显示【num - 1】个,最后一个给到【\0】,也就是对于字符串而言的结束符
  • [x] 若是最大字符数num > 本行的字符数,那么除了显示本行的所有字符之外,还会读入一个换行符,接着就不会往下读了。若是需要读取下一行数据,则需要再次使用这个函数进行读取

在这里插入图片描述

1.3 格式化输入输出【fprintf和fscanf】

接下去来看看有关文件格式化的输入输出

  • 看到这个【fprintf】和【fscanf】是不是又想起来我们之前学的【printf】和【scanf】呢,我们对其进行一个对比。如下图所示
    在这里插入图片描述
  • 既然是进行格式化的输入输出,那我们就来尝试写一些不同格式的内容到文件里去,这里直接定义一个结构体
typedef struct student {
	char name[20];
	int height;
	float score;
}st;
st s = { "zhangsan", 175, 95.5 };
	//写文件
fprintf(pf, "%s %d %f", s.name, s.height, s.score);
  • 可以看到,就写进去了

在这里插入图片描述


  • 然后还是一样,我们要将其读出来

在这里插入图片描述

1.4 二进制输入输出【fwrite和fread】

最后我们来看看有关二进制的读与写操作

  • 首先来完整地了解一下它们该如何操作
    在这里插入图片描述

在这里插入图片描述

  • 看了这些解释之后相信你对二进制文件的读写有了一个基本的概念,接下去我们通过代码来巩固一下

温馨提示:写二进制文件用【wb】,读二进制文件用【rb】

  • 可以看到文件中是写入了一些数据,但是呢写进去的东西是乱码的样子,看不太懂

在这里插入图片描述

  • 不要急,因为我们以二进制的形式写入,自然也要使用二进制的形式读出

在这里插入图片描述

2、拓展:默认打开的三个流

这里给大家拓展一个小知识,也就是对于任何一个C语言程序,只要运行起来,就会默认地打开三个流

  • [x] stdin - 标准输入流 - 键盘
  • [x] stdout - 标准输出流 - 屏幕
  • [x] stderr - 标准输错误 - 屏幕

通过观看源码可以知晓,他们都是以宏定义的形式存放在内存中的,之前我们说过,对于宏定义而言是在程序开始之前就定义好的,也就是当程序运行起来之后,那它们就会存在了

在这里插入图片描述

然后我们去程序中运行一下试试

int ch = fgetc(stdin);
fputc(ch, stdout);
  • 然后可以看到,我们确实可以使用【stdin】和【stdout】这两个流来进行输入和输出

在这里插入图片描述

int ch = 0;
fscanf(stdin, "%c", &ch);
fprintf(stdout, "%c", ch);

在这里插入图片描述

3、对比一组函数【💪】

  • 上面讲到了8个有关文件顺序读写的库函数,接下去给大家对比一下一组函数
  • [x] scanf / fscanf / sscanf
  • [x] printf / fprintf / sprintf

在这里插入图片描述

  • 主要还是来看看【sprintf】和【sscanf】这两个新面貌

在这里插入图片描述
在这里插入图片描述

  • 接下去我们通过代码来看看是不是真的可以实现
char buf[100] = { 0 };
st s = { "zhangsan", 170, 95.5f };
st tmp = { 0 };

//能否将这个结构体的成员转化为字符串
sprintf(buf, "%s %d %f", s.name, s.height, s.score);
printf("%s\n", buf);

//能否将这个字符串中内容还原为一个结构体数据呢
sscanf(buf, "%s %d %f", tmp.name, &(tmp.height), &(tmp.score));

printf("%s %d %f", tmp.name, tmp.height, tmp.score);
  • 可以看到,我将一个结构体数据以格式化的形式写到了一个字符串中,然后又从这个字符串中以格式化的形式读取数据到一个结构体变量中,这么转换来转换去,完全没有问题。你也可以自己去试试

在这里插入图片描述


  • 那可能这么讲还是有点抽象,我们通过一个现实中开发的场景再来描述一下。比如说前端给到用户一个收集信息的表单,用户输入数据之后呢,==前端==就将这些信息用“+”号做了一个拼接给到==后端==,后端呢为了要识别这些信息,一定会创建一个结构体,里面包含这些信息的,这个时候就可以使用到我们上面所说的【sscanf】以格式化的方式去读取这个字符串了,然后就可以解析出用户的这些数据,然后去进行一个处理了
  • 当然在现实的软件开发中,是不会这么去做的,因为有现成封装的API可以调用,库里面会提供一个【序列化/反序列化】的API可以调用,开发者无需考虑其底层的实现
    在这里插入图片描述

五、文件的随机读写

说完了文件的顺序读写,接下去我们来讲讲有关文件的随机读写,这里我会【象征性】地介绍三个有代表性的函数,其他不常用到的我就不介绍了

1、fseek

根据文件指针的位置和偏移量来定位文件指针

  • 首先来看看它的相关介绍
  • 可以看到,最重要的还是最后的那个参数,因为有三个选项可以使用。我会一一介绍
    在这里插入图片描述
  • 然后通过代码我们再来实现一下这个功能。首先看到是使用到了【SEEK_SET】从文件的起始位置开始偏移,因为文件的起始是从第一个字符开始,向后偏移三位就到了【d】的位置

在这里插入图片描述

  • 接下来我们再来看一种。刚才是从前往后偏移,现在则是从后往前偏移,那就要使用到【SEEK_END】

在这里插入图片描述

  • 最后一个是【SEEK_CUR】,也就是从当前位置向后偏移

在这里插入图片描述


==补充一个实际案例==

FILE* pFile;
pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile);
fseek(pFile, 9, SEEK_SET);
fputs(" sam", pFile);
fclose(pFile);
  • 可以看到这里使用了【fseek】,将文件指针偏移到了一个位置,然后从这个位置开始写了一个字符串,运行之后打开文件可以发现里面的英文句子就发现了变化

在这里插入图片描述

2、ftell

返回文件指针相对于起始位置的偏移量

在这里插入图片描述

  • 这个很简单,就是返回当前文件指针所在流中的位置

在这里插入图片描述


==补充一个实际案例==

FILE* pFile;
long size;
pFile = fopen("myfile.txt", "rb");
if (pFile == NULL) perror("Error opening file");
else
{
	fseek(pFile, 0, SEEK_END); //non-portable
	size = ftell(pFile);
	fclose(pFile);
	printf("Size of myfile.txt: %ld bytes.\n", size);
}
  • 这个案例很巧妙地结合了我们上面所学过的【fseek】和【ftell】,求出了这个文件的字节大小

在这里插入图片描述

3、rewind

让文件指针的位置回到文件的起始位置

在这里插入图片描述

  • 可以看到,我们又读到了a,表明文件指针pf确实回到到了起始位置

在这里插入图片描述


==补充一个实际案例==

int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
	fputc(n, pFile);

rewind(pFile);	 //当文件指针pFile重新回到起始位置
fread(buffer, 1, 26, pFile);	//通过文件指针读入26个字母到buffer字符数组中
fclose(pFile);
buffer[26] = '\0';		//'\0'表示字符串的结束位置
puts(buffer);
  • 这个案例就是将1~26个大写英文字母写入文件,然后在让文件指针回到起始位置,在使用二进制的读取方式将文件中的内容读取到字符数组中,最后为字符串设置结束标志,打印出来便是文件中写入的内容

在这里插入图片描述

六、文本文件和二进制文件

接下去我们来谈谈文本文件和二进制文件

  • 【二进制文件】:数据在内存中以二进制的形式存储。不加转换的输出到外存
  • 【文本文件】:以ASCII字符的形式存储的文件。在外存上以ASCII码的形式存储,则需要在存储前转换

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储
在这里插入图片描述
上面就是一个十进制的数值10相关的两种存储形式,我测试了一下,以二进制的形式存放到文件里只占4个字节,但是以ASCLL码的形式存放到文件里就需要占5个字节

  • 接下去我们通过下面这段代码来看看二进制的存储形式
int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

七、文件读取结束的判定

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束

1、被错误使用的eof

去网上看很多的代码可以发现,大家几乎都错误地使用了【feof】这个函数,认为它和==EOF==一样就是用来判断文件是否结束,但是并不是这样,我们一起来探究一下这个函数

  • 从中我们可以知晓【feof】应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
    在这里插入图片描述

2、fgetc、fgets、fscanf、fread结束判断解读

  • 对于上面的四个读取文件函数,要怎么去判断它们是否结束呢?我们通过观察这些函数的返回值来看看

fgetc

  • [x] 如果读取正常,返回读取到的字符的ASCLL码值
  • [x] 如果读取失败,返回EOF

在这里插入图片描述


fgets

  • [x] 如果读取正常,返回读取到的数据的地址
  • [x] 如果读取失败,返回NULL

在这里插入图片描述


fscanf

  • [x] 如果读取正常,返回的是格式串中指定的数据个数
  • [x] 如果读取失败,返回的是小于格式串中指定的数据个数

在这里插入图片描述


fread

  • [x] 如果读取正常,返回的是等于要读取的数据个数
  • [x] 如果读取失败,返回的是小于要读取的数据个数

在这里插入图片描述

3、实例代码走读

接下去大家走读两段代码

==文本文件操作==

  • 首先通过文件指针以读的形式打开了这个文件,然后去判断一下是否打开成功,这里是换了一个形式去判断,和我们在【二叉树】章节判断根结点是否为空是一个道理。(fp == NULL) (root == NULL)
  • 因为条件表达式为真也就是1的时候会进入if判断,那!pf == 1可以推出pf == 0等价于pf == NULL
  • 接下去的话就是去这个文件中一个读取内容然后输出,若是文件到达了EOF,也就是【fgetc】的结束判断条件,此时才可以使用【feof】去进行判断,所以可以看出【feof】是在文件结束之后去判断文件是因为什么而结束的。【ferror】若是成立的话表示这个文件是因为I/O的读取的问题中断的;若不是【feof】判断满足就表示其是正常结束
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
	perror("File opening failed");
	return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
	putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
	puts("I/O error when reading");
else if (feof(fp))
	puts("End of file reached successfully");
fclose(fp);

==二进制文件操作==

  • 这是一个二进制的文件操作,所以以二进制的形式打开,然后使用二进制的写法【fwrite】从a这个数组的首元素地址开始拿取SIZE个大小为double的数据通过fp这个文件指针写出到文件中
    讲一下这里的(sizeof *a)是什么意思,a是数组的首元素地址,然后通过【 * 】解引用可以获取到每个数组元素的大小了
  • 数组a中的数据写入文件后,就要再打开这个文件然后将文件中的内容个读出来,我们将其保存在一个变量中然后对这个变量进行一个判断
  • 因为这个是一个二进制文件,因此我们要去判断它返回的个数是否小于需要读取的个数,若是成立则表示没有读完就结束了,若是和SIZE的个数相同的话表示都读完了,然后我们将读取到数组b中的内容输出一下即可
  • 若是没有读完但是文件又结束了,那么此时使用【feof】判断成立了,将不对的信息打印出来即可,若是没有到达文件末尾但是又读取结束了,进入了【ferror】的判断,表示文件的I/O流出现问题了
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = { 1.,2.,3.,4.,5. };
    double b[SIZE];

    FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
    fclose(fp);

    fp = fopen("test.bin", "rb");
    size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组

    if (ret_code == SIZE) {
        puts("Array read successfully, contents: ");
        for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
        putchar('\n');
    }
    else { // error handling
        if (feof(fp))
            printf("Error reading test.bin: unexpected end of file\n");
        else if (ferror(fp)) {
            perror("Error reading test.bin");
        }
    }

    fclose(fp);
}

好,走读完上面的这个两个实例,相信你对文件操作应该有了一个更进一步的理解,接下去我们来讲讲有关文件缓冲区的知识

八、文件缓冲区

ANSIC 标准采用【缓冲文件系统】处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。

  • [x] 从内存向磁盘输出数据【写】会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
  • [x] 如果从磁盘向计算机【读】入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
  • 缓冲区的大小根据C编译系统决定的

==下面是有关文件缓冲区的示意图==
在这里插入图片描述
==下面是实例代码==

int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);	//先将代码放在输出缓冲区

	printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(10000);
	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
	//注:fflush 在高版本的VS上不能使用了

	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);

	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;
	return 0;
}

在这里插入图片描述
在这里插入图片描述

📚拓展:文件外排序【更上一层楼】

说了这么多有文件的操作,但是不实际去使用还是不行的,纸上得来终觉浅,绝知此事要躬行。接下去就让我们来看看如何对一个文件中的数据进行排序

1、前言

  • 对于文件中的数据,一般都是很大的,不像我们上面所讲的十二十个数,可能会有成千上百的数据需要我们去排序,此时效率最高的就是【归并排序】了,因为面对海量的数据而言,像效率较高的【快速排序】需要克服三数取中的困难,还有像【堆排序】【希尔排序】这些,都无法支持随机访问,所以很难去对大量的文件进行一个排序,速度会非常之慢。即使是有文件函数【fseek()】这样的函数可以使文件指针偏移,还是很难做到高效。因为磁盘的速度比起内存差了太多太多了,具体的我不太清楚大概有差个几千倍这样,
  • 所以我们就想到了【归并排序】,它既是内排序,也是外排序,而且性能也不差,算是速度较快的几个排序之一了。但是要如何进行归并呢?

2、思路解析

在这里插入图片描述

  • 回忆一下归并排序的原理,就是两个有序区有序,然后两两一归才使得整体可以有序,如果左右都无需,那么继续对其进行左右分割归并
  • 但是本次,我要教给你的你是另外一种思路:

将一个大文件平均分割成N份,保证每份的大小可以加载到内存中,然后使用快排将其排成有序再写回一个个小文件,此时就拥有了文件中归并的先决条件

  • 具体示意图如下

在这里插入图片描述
==这里我设置一个这样的规则,令文件1为【1】,文件2位【2】,它们归并之后即为【12】,然后再让【12】和文件3即【3】归并变成【123】,以此类推,所以最后归出的文件名应该是【12345678910】==

3、代码详解

下面是大文件分割成10个小文件的逻辑,首先来讲解一下这块,代码中很多内容涉及到文件操作,如果有文件操作还不是很懂的小伙伴记得再去温习一下

  • 整体的逻辑就在于从文件中读取100个数据,但是分批进行读取,每次首先去读9个数,然后当读到第十个数的时候,先将其加入数组中,然后再对数组中的这10个数进行排序。排完序后就将这个10个数通过文件指针再写到一个小文件中
  • 接着当第二次循环上来的时候,就开始读第11~20个数;以此往复,直到读完这个100个数为止,那此时我们的工程目录下就会出现10个小文件,就是对这100个数的分隔排序后的结果
void MergeSortFile(const char* file)
{
	FILE* fout = fopen(file, "r");
	if (!fout)
	{
		perror("fopen fail");
		exit(-1);
	}
	
	int num = 0;
	int n = 10;
	int i = 0;
	int b[10];
	char subfile[20];
	int filei = 1;
	//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
	memset(b, 0, sizeof(int) * n);
	while (fscanf(fout, "%d\n", &num) != EOF)
	{
		if (i < n - 1)
		{
			b[i++] = num;	//首先读9个数据到数组中
		}
		else
		{
			b[i] = num;		//再将第十个输入放入数组
			QuickSort(b, 0, n - 1);		//对其进行排序

			sprintf(subfile, "%d", filei++);

			FILE* fin = fopen(subfile, "w");
			if (!fin)
			{
				perror("fopen fail");
				exit(-1);
			}
			//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
			for (int j = 0; j < n; ++j)
			{
				fprintf(fin, "%d\n", b[j]);
			}
			fclose(fin);

			i = 0;		//i重新置0,方便下一次的读取
			memset(b, 0, sizeof(int) * n);
		}
	}
  • 我们来看一下排序的结果

在这里插入图片描述

  • 将大文件分成10个小文件后,接下去就是要对这个10个小文件进行归并,具体规则我上面已经说了
  • 下面就是单趟归并的逻辑的,就和我们上面说到的归并排序的代码是很类似的,只不过这里是文件的操作而已。要注意的是对于文件来说是有一个文件指针的,若是你读取了一个之后那么文件指针这个结构体中的数据标记就会发生变化,标记为当然所读内容的下一个了
  • 所以我们不能将读取读取小文件中的数据的操作放在while循环中,应该单独将其抽离出来进行判断才才对。若是哪个文件中的数小,那么就将这个数写到新的【mfile】文件中去,然后继续读取当前文件的后一个内容
//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
	FILE* fout1 = fopen(file1, "r");
	if (!fout1)
	{
		perror("fopen fail");
		exit(-1);
	}

	FILE* fout2 = fopen(file2, "r");
	if (!fout2)
	{
		perror("fopen fail");
		exit(-1);
	}

	FILE* fin = fopen(mfile, "w");
	if (!fin)
	{
		perror("fopen fail");
		exit(-1);
	}

	int num1, num2;
	//返回值拿到循环外来接受
	int ret1 = fscanf(fout1, "%d\n", &num1);
	int ret2 = fscanf(fout2, "%d\n", &num2);
	while (ret1 != EOF && ret2 != EOF)
	{
		if (num1 < num2)
		{
			fprintf(fin, "%d\n", num1);
			ret1 = fscanf(fout1, "%d\n", &num1);
		}
		else
		{
			fprintf(fin, "%d\n", num2);
			ret2 = fscanf(fout2, "%d\n", &num2);
		}
	}

	while (ret1 != EOF)
	{
		fprintf(fin, "%d\n", num1);
		ret1 = fscanf(fout1, "%d\n", &num1);
	}
	while (ret2 != EOF)
	{
		fprintf(fin, "%d\n", num2);
		ret2 = fscanf(fout2, "%d\n", &num2);
	}

	fclose(fout1);
	fclose(fout2);
	fclose(fin);
}

最后在打开文件后不要忘了将文件关闭哦,不然就白操作了

  • 当然上面是一个单趟的逻辑,我们还要对【file1】【file2】【mfile】进行一个迭代
//利用互相归并到文件,实现整体有序
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; ++i)
{
	_MergeSortFile(file1, file2, mfile);
	
	//迭代
	strcpy(file1, mfile);
	sprintf(file2, "%d", i + 1);
	sprintf(mfile, "%s%d", mfile, i + 1);
	
}
  • 大概就是这么一个迭代的过程

在这里插入图片描述
在这里插入图片描述

==整体代码展示==

//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
	FILE* fout1 = fopen(file1, "r");
	if (!fout1)
	{
		perror("fopen fail");
		exit(-1);
	}

	FILE* fout2 = fopen(file2, "r");
	if (!fout2)
	{
		perror("fopen fail");
		exit(-1);
	}

	FILE* fin = fopen(mfile, "w");
	if (!fin)
	{
		perror("fopen fail");
		exit(-1);
	}

	int num1, num2;
	//返回值拿到循环外来接受
	int ret1 = fscanf(fout1, "%d\n", &num1);
	int ret2 = fscanf(fout2, "%d\n", &num2);
	while (ret1 != EOF && ret2 != EOF)
	{
		if (num1 < num2)
		{
			fprintf(fin, "%d\n", num1);
			ret1 = fscanf(fout1, "%d\n", &num1);
		}
		else
		{
			fprintf(fin, "%d\n", num2);
			ret2 = fscanf(fout2, "%d\n", &num2);
		}
	}

	while (ret1 != EOF)
	{
		fprintf(fin, "%d\n", num1);
		ret1 = fscanf(fout1, "%d\n", &num1);
	}
	while (ret2 != EOF)
	{
		fprintf(fin, "%d\n", num2);
		ret2 = fscanf(fout2, "%d\n", &num2);
	}

	fclose(fout1);
	fclose(fout2);
	fclose(fin);
}

/*文件外排序*/
void MergeSortFile(const char* file)
{
	srand((unsigned int)time(NULL));
	FILE* fout = fopen(file, "r");
	if (!fout)
	{
		perror("fopen fail");
		exit(-1);
	}

	//先写100个随机数进文件
	//for (int i = 0; i < 100; ++i)
	//{
	//	int num = rand() % 100;
	//	fprintf(fout, "%d\n", num);
	//}

	int num = 0;
	int n = 10;
	int i = 0;
	int b[10];
	char subfile[20];
	int filei = 1;

	//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
	memset(b, 0, sizeof(int) * n);
	while (fscanf(fout, "%d\n", &num) != EOF)
	{
		if (i < n - 1)
		{
			b[i++] = num;	//首先读9个数据到数组中
		}
		else
		{
			b[i] = num;		//再将第十个输入放入数组
			QuickSort(b, 0, n - 1);		//对其进行排序

			sprintf(subfile, "%d", filei++);

			FILE* fin = fopen(subfile, "w");
			if (!fin)
			{
				perror("fopen fail");
				exit(-1);
			}
			//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
			for (int j = 0; j < n; ++j)
			{
				fprintf(fin, "%d\n", b[j]);
			}
			fclose(fin);

			i = 0;		//i重新置0,方便下一次的读取
			memset(b, 0, sizeof(int) * n);
		}
	}

	//利用互相归并到文件,实现整体有序
	char file1[100] = "1";
	char file2[100] = "2";
	char mfile[100] = "12";
	for (int i = 2; i <= n; ++i)
	{
		_MergeSortFile(file1, file2, mfile);
		
		//迭代
		strcpy(file1, mfile);
		sprintf(file2, "%d", i + 1);
		sprintf(mfile, "%s%d", mfile, i + 1);
		
	}

}

==运行结果展示==

在这里插入图片描述

九、总结与提炼

好,我们来总结回顾一下本文所学习的知识

在本文中我们了解了什么是文件,知道了有【数据文件】和【文本文件】两种,接下去主要是对数据文件展开了一系列的操作:

  • 首先是说到如何去打开和关闭一个文件,以及打开文件的一些方式。之后讲解了有关文件的顺序读写,里面说到了对文件进行读写的八个常用函数 【fgetc】【fputc】【fgets】【fputs】【fscanf】【fprintf】【fread】【fwrite】,说完顺序读写后我们又讲了随机读写,也给大家讲了三个函数 【fseek】【ftell】【rewind】。其他函数记不住没关系,这是一个函数希望大家可以牢记于心,这样你的文件操作就能更加熟练
  • 其次我们又说到了有关【feof】的错误使用,讲解了其该如何去正确使用,这个函数也希望你可以记住,在进行文件判断是否读取完毕的时候可以起到很关键的作用
  • 最后,我们又说到了如何对一个文件中的数据去进行排序,充分地将学习的知识运用到了实际的应用中,希望通过这个你对文件的操作可以更上一层楼

最后很感谢您对本文的观看,如有疑问,请于评论区留言或者私信我可以:cherry_blossom:

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。