C生万物之文件操作

文章目录

    • 一、为什么使用文件?
    • 二、什么是文件?
      • 1、程序文件
      • 2、数据文件
      • 3、文件名
    • 三、文件的打开和关闭
      • 1、文件指针
      • 2、文件的打开和关闭
    • 四、文件的顺序读写
      • 1. 8个重要的库函数
        • 1.1 单字符输入输出【fputc和fgetc】
        • 1.2 文本行输入输出【fputs和fgets】
        • 1.3 格式化输入输出【fprintf和fscanf】
        • 1.4 二进制输入输出【fwrite和fread】
      • 2、拓展:默认打开的三个流
      • 3、对比一组函数
    • 五、文件的随机读写
      • 1、fseek
      • 2、ftell
      • 3、rewind
    • 六、文本文件和二进制文件
    • 七、文件读取结束的判定
      • 1、被错误使用的feof
      • 2、fgetc、fgets、fscanf、fread结束判断解读
      • 3、实例代码走读
    • 八、文件缓冲区
    • 拓展:文件外排序
      • 1、前言
      • 2、思路解析
      • 3、代码详解

一、为什么使用文件?

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

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

二、什么是文件?

  • 磁盘上的文件是文件

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

1、程序文件

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

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

2、数据文件

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

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

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

3、文件名

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

  • 文件名包含3部分:文件路径+文件名主干+文件后缀

  • 例如: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、文件的打开和关闭

注:文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

  • 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()
{
	//打开文件
	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里面找到这两个函数的描述
int fputc ( int character, FILE * stream );
int fgetc ( FILE * stream );

在这里插入图片描述

在这里插入图片描述

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

  • 首先是写文件,我们往【test.txt】中写一个字符a进去
#include<stdio.h>
int main()
{
	//打开文件
	FILE* pf = fopen("data.txt", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fputc('a', pf);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

在这里插入图片描述

  • 既然能写一个,那也就可以写多个

在这里插入图片描述

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

在这里插入图片描述


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

在这里插入图片描述

  • 能读一个,那也能读多个,我们多读几个试试
int ch = fgetc(pf);
printf("%c", ch);

ch = fgetc(pf);
printf("%c", ch);

ch = fgetc(pf);
printf("%c", ch);

ch = fgetc(pf);
printf("%c", ch);

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

1.2 文本行输入输出【fputs和fgets】
  • 首先来了解一下这个两个函数

fputs
fgets

int fputs ( const char * str, FILE * stream );
char * fgets ( char * str, int num, FILE * stream );

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

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

在这里插入图片描述

  • 接下去多写几行试试

在这里插入图片描述


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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

fprintf
fscanf

  • 看到这个【fprintf】和【fscanf】是不是又想起来我们之前学的【printf】和【scanf】呢,我们对其进行一个对比。如下图所示
int fprintf ( FILE * stream, const char * format, ... );
int fscanf ( FILE * stream, const char * format, ... );

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

  • 既然是进行格式化的输入输出,那我们就来尝试写一些不同格式的内容到文件里去,这里直接定义一个结构体
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】

fwite
fread

然后继续看一下

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

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

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

在这里插入图片描述

  • 既然都是以二进值写的,那么我们也需要以二进值读
    可以看到,以二进值的方式读出来了
    在这里插入图片描述

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

对于任何一个C语言程序,只要运行起来,就会默认地打开三个流

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

3、对比一组函数

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

在这里插入图片描述

  • 主要还是来看看【sprintf】和【sscanf】这两个新面貌
int sprintf ( char * str, const char * format, ... );

在这里插入图片描述

int sscanf ( const char * s, const char * format, ...);

在这里插入图片描述

  • 接下去我们通过代码来看看是不是真的可以实现
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

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

fseek

  • 首先来看看它的相关介绍
  • 可以看到,最重要的还是最后的那个参数,因为有三个选项可以使用。
int fseek ( FILE * stream, long int offset, int origin );

在这里插入图片描述

  • 然后通过代码我们再来实现一下这个功能。首先看到是使用到了【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);
  • 偏移到This is a然后修改后面的内容

在这里插入图片描述

2、ftell

fteel

  • 返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );

在这里插入图片描述

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

在这里插入图片描述
补充一个实际案例

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

rewind

  • 让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );

在这里插入图片描述

  • 可以看到,我们又读到了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、被错误使用的feof

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

  • 从中我们可以知晓【feof】应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

    在这里插入图片描述

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

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

fgetc

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

在这里插入图片描述


fgets

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

在这里插入图片描述


fscanf

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

在这里插入图片描述


fread

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

在这里插入图片描述

3、实例代码走读

文本文件操作

  • 首先通过文件指针以读的形式打开了这个文件,然后去判断一下是否打开成功。(fp == 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()
{
    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 标准采用【缓冲文件系统】处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。

  • 从内存向磁盘输出数据【写】会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。

  • 如果从磁盘向计算机【读】入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

  • 缓冲区的大小根据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);
	}
}

运行结果展示

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/780475.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

robotframework-appiumLibrary 应用 - 实现 app 自动化

1、安装appiumLibrary第三方库 运行pip命令&#xff1a;pip install robotframework-appiumlibrary 若已安装&#xff0c;需要更新版本可以用命令&#xff1a;pip install -U robotframework-appiumlibrary 2、安装app自动化环境。 参考我的另外一篇专门app自动化环境安装的…

elastic-job 定时任务 —— elasticjob 介绍与使用教程

文章目录 Elastic-Job 介绍相关依赖elastic-job 目录结构SimpleJob 简单作业编码下载并启动 ZooKeeper编写定时任务代码并启动 Elastic-Job 介绍 概述&#xff1a; Elastic-Job 是当当网开源的一个分布式调度解决方案&#xff0c;基于 Quartz 二次开发的&#xff0c;由两个相…

科普新能源充电桩

充电桩是新能源电动车的配套基础设施&#xff0c;为电动车提供充电服务&#xff0c;与我们的生活也是息息相关&#xff0c;本篇文章来科普一下充电桩基础知识。 充电桩的分类 按照供电方式分类 交流充电桩&#xff1a;特点是小电流、桩体较小、安装灵活&#xff1b;直流充电…

Linux shell编程学习笔记63:free命令 获取内存使用信息

0 前言 在系统安全检查中&#xff0c;内存使用情况也是一块可以关注的内容。Linux提供了多个获取内存信息的命令很多。今天我们先研究free命令。 1 free命令的功能、用法和选项说明 1.1 free命令的功能 free 命令可以显示系统内存的使用情况&#xff0c;包括物理内存、交换…

论文1:多模态人类活动识别综述

论文题目&#xff1a;A Review of Multimodal Human Activity Recognition with Special Emphasis on Classification, Applications, Challenges and Future Directions 文献偏旧-2021 1、 专业词汇&#xff1a; Human activity recognition (HAR)-人类活动识别 Wearable …

android中activity与fragment之间的各种跳转

我们以音乐播放、视频播放、用户注册与登录为例【Musicfragment&#xff08;音乐列表页&#xff09;、Videofragment&#xff08;视频列表页&#xff09;、MusicAvtivity&#xff08;音乐详情页&#xff09;、VideoFragment&#xff08;视频详情页&#xff09;、LoginActivity&…

时钟资源(参考ug472)

目录 时钟资源(参考ug472)7系列 FPGA 时钟连接差异时钟资源连接关系表时钟资源连接示意图不同时钟区域资源连接图Clock-Capable Inputs介绍布局规则 全局时钟 bufferBUFGCTRL介绍原语参数及端口INIT_OUTPRESELECT_I0/1I0/1CE0/1S0/1IGNORE0/1 真值表时序 BUFGBUFGCE&#xff0c…

日本服务器托管需要注意哪些问题

日本服务器托管是一项涉及多方面因素的重要决策&#xff0c;为了确保托管服务的稳定、高效与安全&#xff0c;企业或个人在托管过程中需要注意以下几个关键问题&#xff1a; 首先&#xff0c;数据中心的基础设施建设标准是决定托管稳定性的关键。这包括数据中心的建筑抗震、抗洪…

单片机关键任务优先级的实现学习

与总体产品联调时&#xff0c;需要各个单机系统严格按照总体要求&#xff0c;进行数据输出&#xff0c;时间的偏差将出现系统异常&#xff0c;控制失败等不稳定情况产生&#xff0c;甚至影响到产品安全。 因此必须确保某些关键任务的优先执行。单片机任务优先级一般有两种方式…

Java 基础知识之 switch 语句和 yield 关键字

传统 switch 语句 传统的 switch 语句我们已经写了一万遍了&#xff0c;以下是一个典型的 switch 语句&#xff1a; int dayOfWeek 3; switch (dayOfWeek) {case 1:System.out.println("星期一");break;case 2:System.out.println("星期二");break;case…

STM32-I2C

本内容基于江协科技STM32视频学习之后整理而得。 文章目录 1. I2C通信1.1 I2C通信简介1.2 硬件电路1.3 I2C时序基本单元1.3.1 起始条件和终止条件1.3.2 发送一个字节1.3.3 接收一个字节1.3.4 发送应答和接收应答 1.4 I2C时序1.4.1 指定地址写1.4.2 当前地址读1.4.3 指定地址读…

Java应用系统设计与实现--学生信息管理系统(附解决方案源码)

一、实验目的及要求 1.1实验目的 掌握Java GUI编程技术&#xff0c;了解Swing框架的使用。 掌握MySQL数据库的基本操作&#xff0c;了解如何在Java中连接和操作数据库。 掌握用户权限管理的基本概念和实现方法。 提升综合运用所学知识设计和实现一个完整应用系统的能力…

QThread moveToThread的妙用

官方文档描述 总结就是移动到线程的对象不能有父对象&#xff0c;执行start即起一个线程&#xff0c;示例是将myObject移动到主线程中。QT中这种方式起一个线程是非常简单的。 示例描述以及代码 描述往Communicate线程中频繁添加任务&#xff0c;等任务结束的时候统计计算的结…

【python教程】数据分析——numpy、pandas、matplotlib

【python教程】数据分析——numpy、pandas、matplotlib 文章目录 什么是matplotlib安装matplotlib&#xff0c;画个折线 什么是matplotlib matplotlib:最流行的Python底层绘图库&#xff0c;主要做数据可视化图表,名字取材于MATLAB&#xff0c;模仿MATLAB构建 安装matplotlib&…

Node 中基于 Koa 框架的 Web 服务搭建实战

前言 在《Node之Web服务 - 掘金 (juejin.cn)》一文中,我们使用 HTTP 模块构建了后端接口,从而实现了后端服务的开发。可以对此进行进一步优化 http模块代码回顾 const http require("http");const server http.createServer((req, res) > {if (reqUrl.pathna…

【面试八股文】java基础知识

引言 本文是java面试时的一些常见知识点总结归纳和一些拓展&#xff0c;笔者在学习这些内容时&#xff0c;特地整理记录下来&#xff0c;以供大家学习共勉。 一、数据类型 1.1 为什么要设计封装类&#xff0c;Integer和int区别是什么&#xff1f; 使用封装类的目的 对象化:…

阶段三:项目开发---搭建项目前后端系统基础架构:任务13:实现基本的登录功能

任务描述 任务名称&#xff1a; 实现基本的登录功能 知识点&#xff1a; 了解前端Vue项目的基本执行过程 重 点&#xff1a; 构建项目的基本登陆功能 内 容&#xff1a; 通过实现项目的基本登录功能&#xff0c;来了解前端Vue项目的基本执行过程&#xff0c;并完成基…

前端面试题17(js快速检索方法详解)

在前端JavaScript中&#xff0c;快速检索数据通常涉及到数组或对象的搜索。这里我会介绍几种常见的快速检索方法&#xff0c;并提供相应的代码示例。 1. 数组的find和findIndex方法 find: 返回数组中满足条件的第一个元素的值。findIndex: 返回数组中满足条件的第一个元素的索…

基于LSTM的股票价格预测

摘要 本课设旨在利用LSTM&#xff08;长短期记忆&#xff09;网络实现股票价格预测&#xff0c;通过收集、预处理股票数据集&#xff0c;并构建预测模型进行训练与优化。实验结果显示&#xff0c;经过优化调整模型参数&#xff0c;模型在测试集上取得了较为理想的预测效果。尽…

《征服数据结构》SparseArray

摘要&#xff1a; 1&#xff0c;SparseArray的介绍 2&#xff0c;SparseArray的代码实现 1&#xff0c;SparseArray的介绍 前面我们讲过《ArrayMap》&#xff0c;用它来实现哈希表&#xff0c;其中存放key和value的数组长度是存放散列表数组长度的二倍。 在哈希表中如果key值是…