我们之前在做通讯录的时候会遇到一个问题那就是当我们退出程序之后,再次进入那么之前储存的信息就会消失。那是因为我们之前所写的那个通讯录只是将数据储存在内存里面,当我们退出程序之后那些信息就会被操作系统清理。而为了储存那些信息我们就要将信息储存到我们的硬盘(文件)中去,那么当我们下一次运行程序时再从那个文件中读取之前的信息。至于这个功能如何实现我们放到最后我们先来简略认识一下文件操作函数

文件操作函数详解_#include

我们既然要写数据到一个文件当中,那么肯定首先就是要将文件先打开。

fopen函数

文件操作函数详解_数据_02

从图片我们能看到这个函数返回的是一个FILE类型的指针,那么这个FILE是个什么类型呢?

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

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

画图表示

文件操作函数详解_打开文件_03

就是通过这个指针内存就能访问到指定的文件了。

那么const char * filename这个参数又代表什么呢?

这个就代表路径也就是文件所储存的地方,如果你只写了文件名的话那么系统就默认只在当前工程文件中寻找,如果你想打开其它位置的文件的话那么就要将文件的路径和文件名加入。

const char * mode这个参数就代表以什么的方式使用文件。

使用文件方式

含义

若指定文件不存在

“r”(只读)

为了输入数据,打开一个已经存在的文本文件

出错

“w”(只写)

为了输出数据,打开一个文本文件

建立一个新的文件


“a”(追加)

向文本文件尾添加数据

建立一个新的文件

“rb”(只读)

为了输入数据,打开一个二进制文件

出错

“wb”(只写)

为了输入数据,打开一个已经存在的二进制文件

建立一个新的文件

“ab”(追加)

向一个二进制文件尾添加数据

出错

“r+”(读写)

为了读和写,打开一个文本文件

出错

“w+”(读写)

为了读和写,建议一个新的文件

建立一个新的文件

“a+”(读写)

打开一个文件,在文件尾进行读写

建立一个新的文件

“rb+”(读写)

为了读和写打开一个二进制文件

出错

“wb+”(读写)

为了读和写,新建一个新的二进制文件

建立一个新的文件

“ab+”(读写)

打开一个二进制文件,在文件尾进行读和写

建立一个新的文件

从这里我们也就能知道了fopen打开文件是可能会出错的所以和malloc函数一样我们要判断它的返回值

如果打开失败了,这个函数返回的就是一个空指针。

我们这里就先跳过写文件讲解一下当文件使用完毕后我们要怎么去关闭一个文件,要使用的函数也就是 fclose函数。

文件操作函数详解_打开文件_04

从图片我们也可以知道这个函数需要的也就是一个文件指针。和free函数使用方法是一样的。所以我们最后也要将那个FILE*类型的指针置为空,防止野指针的出现。

现在我们就来学习读写文件操作

从最上面的思维导图我们也能看到读写文件分为文件的顺序读写和文件的随机读写。

我们先来学习文件顺序读写的函数:

              功能                                             函数名                                       适用于

字符输出函数

fputc

所有输出流

字符输入函数

fgetc

所有输入流

文本行输入函数

fgets

所有输入流

文本行输出函数

fputs

所有输出流

格式化输入函数

fscanf

所有输入流

格式化输出函数

fprintf

所有输出流

二进制输入

fread

文件

二进制输出

fwrite

文件

我们先来介绍fputc这是一个字符输出函数,我们这里要怎么理解输出呢?即我们要以内存的视角去看我们现在要将内存中的字符数据储存到一个文件中去,所以这就是一个字符输出函数

我们下面通过一个代码来学习使用这个函数:

#include<stdio.h>
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "w");//这个代码的意思就是我们以写的方式打开名为test.txt的文件,若工程文件中没有这个文件
	//则会建立一个新文件,如果存在这个文件(且文件中有数据)那么这个代码就会将
    //文件中的信息给清理
	//判断是否真的打开
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//写文件这里我们就向这个文件中写字符a
	fputc('a', p);
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

文件操作函数详解_打开文件_05

可以看到在工程文件中确实出现了名叫test.txt的文件,并且里面保存的就是a

那么我们能不能将这个a从文件中提取出来,再打印出来呢?当然可以

看如下代码:

#include<stdio.h>
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "r");//这个代码的意思就是我们以读的方式打开名为test.txt的文件,若工程文件中没有这个文件
	//则会出错返回一个空指针
	//判断是否真的打开
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//写文件这里我们就从文件中读字符a
	int ch =fgetc(p);//这个函数返回的是读取字符的ascll值所以我们用一个int的变量储存这个值
	printf("%c", ch);//以字符形式打印这个字符
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

文件操作函数详解_数据_06

那么我们能不能将26个字母输入到文件中,再通过文件将26个字母提取出来呢?

当然可以

我们先通过这个代码将a到z的信息储存到文件中去

#include<stdio.h>
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "w");
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
    //写文件
	for (int i = 0; i < 26; i++)
	{
		fputc('a' + i, p);
	}
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

文件操作函数详解_打开文件_07

我们再通过下面这个代码将这个文件中的a到z的数据拿出来并打印

#include<stdio.h>
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "r");
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//写文件
	for (int i = 0; i < 26; i++)
	{
		int ch = fgetc(p);
		printf("%c", ch);
	}
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

文件操作函数详解_打开文件_08

下面我们来学习文本行输出函数fputs

就和名字一样这个函数的作用也就是将一个字符串写入到文件中去,

文件操作函数详解_#include_09

从图片我们就能知道了,这个函数需要两个参数一个就是要输到文件中的字符传的地址,而另外一个自然也就是文件指针了。

我们这里就将王五张三输出到文件中去。

#include<stdio.h>
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "w");//以写的形式打开
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//写文件
	char te[20] = "王五张三";
	fputs(te, p);
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

文件操作函数详解_#include_10

我们再通过fgets从文件中读取这些信息。

文件操作函数详解_数据_11

可以看到这个函数需要三个参数,第一个参数也就是将读取到的文件储存到哪一个变量中去,

第二个参数也就是最多读取几个数据,第三个参数也就是从哪个文件指针中读取。

代码运行

#include<stdio.h>
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "r");//以读的形式打开
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//写文件
	char te[20] = {0};
	fgets(te, 3, p);
	printf("%s", te);
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

这里因为我用的是汉字所以不能看出fgets函数的一个特点,这个特点就是如果你输入到文件中的是zhangsan,然后你读取3个然后打印,你最后打印出的一定是zh因为读取的时候默认最后一个会作为\0被读取。

文件操作函数详解_数据_12

那么除了字符和字符串以外我们还有整型浮点型那样的数据也能存入到文件中吗?

答案当然是肯定的。那就是格式化的输入和输出函数fprintf和fscanf,我们先来学习fprintf

通过下面的代码来实现

很明显我们已经通过这个函数将结构体里面格式化的数据写入到文件中去了。

#include<stdio.h>
typedef struct stu
{
	char name[20];
	int num;
	char sex;
}s;
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "w");//以写的形式打开文件
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//写文件
	s s1 = { "wangwu",1234,'M' };//我们现在就将这一个结构体的数据存入到文件中去
	fprintf(p,"%s %d %c", s1.name, s1.num, s1.sex);
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

文件操作函数详解_打开文件_13

现在我们再通过格式化的输入函数fscanf函数将文本中的数据再以格式化的形式提取出来。

代码

#include<stdio.h>
typedef struct stu
{
	char name[20];
	int num;
	char sex;
}s;
int main()
{
	//打开文件
	FILE* p = fopen("test.txt", "r");//以读的形式打开文件
	if (p == NULL)
	{
		perror("fopen");//将错误信息打印出来
		return 1;//已经打开文件失败了自然就不能继续往下运行了
	}
	//读文件
	s s1 = {0};//现在在这个结构体当中没有任何的数据
	fscanf(p, "%s %d %c", s1.name, &(s1.num), &(s1.sex));//从p中读取数据放到s1中
	printf("%s %d %c", s1.name, s1.num, s1.sex);
	//关闭文件
	fclose(p);////关闭文件
	p = NULL;//防止野指针的出现
	return 0;
}

运行成功结果正确

文件操作函数详解_#include_14

我在这里并没有说fscanf和fprintf这两者在参数上的要求是什么,因为我们在使用这两个函数的时候就假设我们在使用printf和scanf,因为fscanf,fprintf和scanf,printf在参数的区别上就只有那个文件指针而已。

既然都已经说到了这里那么我们就来学习一下sscanf和sprintf

sscanf

文件操作函数详解_打开文件_15

sprintf

文件操作函数详解_打开文件_16

sscanf作用是将字符串中不同格式的数据转化成对应的格式化数据,即从字符串中读取数据

sprintf作用是将不同格式的数据输入到一个字符串中去(把一个格式化的数据转化为字符串),例如将数字1或2输入到一个字符串中去

代码如下:

#include<stdio.h>
typedef struct stu
{
	char name[20];
	int num;
	float score;
}s;
int main()
{
	char name[100] = { 0 };//这里我并没有给这个字符串赋值
	s s1 = { "zhangsan",1001,99.5f };
	sprintf(name, "%s %d %f", s1.name, s1.num, s1.score);
	printf("%s", name);//最后打印出来了值
	return 0;
}

文件操作函数详解_数据_17

我们现在来试一下将这个字符串中的格式化数据提取出来,

代码

#include<string.h>
typedef struct stu
{
	char name[20];
	int num;
	float score;
}s;
int main()
{
	char name[100] = { 0 };
	s s1 = { "zhangsan",1001,99.5f };
	sprintf(name, "%s %d %f", s1.name, s1.num, s1.score);
	printf("sprintf:%s\n", name);
	memset(s1.name, 0, sizeof(s1.name));//经过这一次内存设置s1中的name数组就被设置为了0
	s1.score = 0; s1.num = 0;//s1被全置为了0
	sscanf(name,"%s %d %f", s1.name,&(s1.num),&(s1.score));//这里就是将字符串中的格式化数据提取出来
	printf("sscanf:%s %d %f", s1.name, s1.num, s1.score);
	return 0;
}

文件操作函数详解_数据_18

现在我们已经将上面那个表里面大部分的函数都已讲解完毕,那么我们再来看一下这个表

              功能                                             函数名                                       适用于

字符输出函数

fputc

所有输出流

字符输入函数

fgetc

所有输入流

文本行输入函数

fgets

所有输入流

文本行输出函数

fputs

所有输出流

格式化输入函数

fscanf

所有输入流

格式化输出函数

fprintf

所有输出流

二进制输入

fread

文件

二进制输出

fwrite

文件

这个表最后的这个适用于里面说的流又是什么东西呢?

  1. 流是一个抽象的概念,是对各种输入输出设备的抽象描述,使得编程的逻辑更为清晰;
  2. 流是有方向的,在C语言中分为输入流和输出流;
  3. 流与设备分离,这意味着不同的输入输出设备可以使用相同的流操作函数来进行读写操作,且可以方便地将程序的输入输出重定向到不同的设备上;
  4. 流是一个缓冲区,输入流从缓冲区中读取数据,输出流将数据写入缓冲区,然后统一发送到目标设备。


假设我们程序员想将数据传输到文件,屏幕,网络等等的外部设备上时,我们改怎么办呢?这个时候我们抽象出了一个流的概念,我们只用将数据输出到这个流里面然后这个流就会将数据自动的送到所需要的外部设备上。

我们要认识到下面的这些流:

读写文件的时候:文件流

我们的终端(屏幕)也叫做标准的输出流:stdout

我们的键盘也叫做标准输入流:stdin

标准错误流stderr

这三个流的默认类型都是FILE*

而当我们在写一个c程序的时候,默认是开启这三个流(标准输出流,标准输入流,标准错误流)

而正因为这三个流是默认打开的,所以我们在使用scanf函数的时候是从键盘(默认输入流去读取数据的),同理printf也是。

那么我们能不能通过使用标准输入流和标准输出流呢?

答案当然是肯定的。

我们来看fgetc这个函数既然适用于所有的输入流,自然也就能直接从标准输入流(键盘)中读取数据。

代码:

#include<stdio.h>
int main()
{  
	int ch = fgetc(stdin);//这里我们就是从标准输入流直接读取数据,然后读取到的ascll值被返回给了ch
	printf("%c", ch);
	return;
}

当然我这里只是使用了fgtc,那么同理fgets,fscanf,自然也是可以的

文件操作函数详解_#include_19

下面我们来运用标准输出流

#include<stdio.h>
int main()
{
	fputc('a', stdout);//我们将字符a直接传递给标准输出流打印
	return 0;
}

文件操作函数详解_数据_20

至于其它的函数我们自然也可以使用,只用将FILE* stream换成标准输入或是标准输出流就可以了。

那么对于文件的顺序读写我们还剩两个函数(fwrite和fread函数)从图我们能知道这两个函数适用于文件,但是它的功能又是二进制的输入和输出。我们要怎么取理解呢?

首先是fwrite

文件操作函数详解_#include_21

首先这个函数需要的是四个参数,第一个参数也就是我们要写的数据的地址

第二个也就是数据的大小,第三个也就是输出数据的个数,第四个也就是文件指针了

那么这个函数功能也就是将从ptr指向的空间里将count个的大小为size的数据放入stream对应的文件里面。

通过代码

可以看到在记事本里面确实保存了信息,但是除了最开始的字符串以外,我们压根看不懂这就是因为我们以二进制的形式写入。我们这里先不解释为什么会出现这些符号。我们继续看我们能不能通过fread函数将其中储存的信息打印出来。

文件操作函数详解_数据_22


fread

文件操作函数详解_#include_23

从图片我们也能知道这个函数的参数和上面的函数参数是一样的,但是作用不同上一个函数(fwrite)是将信息从ptr指向的空间输出到stream对应的文件里,fread函数则是从stream里面读取最多count个大小为size的数据放到ptr指向的空间里面。

#include<stdio.h>
typedef struct stu
{
	char name[20];
	int num;
	float score;
}s;
int main()
{
	//打开文件
	FILE* pc = fopen("test.txt", "rb");//这里我们以二进制写的形式读这个文件
	if (pc == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	s s1 = { 0 };//这里面我已将s1置为空
	fread(&s1, sizeof(s), 1, pc);
	printf("%s %d %f", s1.name, s1.num, s1.score);
	//关闭文件
	fclose(pc);
	pc = NULL;
	return 0;
}

运行成功

文件操作函数详解_打开文件_24

欧克。

那么为什么我们在记事本里面会看到那一串符号呢?

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。 如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文 本文件。 一个数据在内存中是怎么存储的呢? 字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。 如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节

文件操作函数详解_#include_25

那么我们首先要将10000以二进制的形式存到文本中去

代码

#include<stdio.h>
int main()
{
	FILE* pc = fopen("test.txt", "wb");
	if (pc == NULL)
	{
		perror("fopen");
		return 1;
	}
	int a = 10000;
	fwrite(&a, sizeof(int), 1, pc);
	fclose(pc);
	pc = NULL;
	return 0;
}

文件操作函数详解_打开文件_26

现在已经将1000存到了test.txt中接下来我们就来看一看test.txt里面存的二进制码

文件操作函数详解_打开文件_27

可以看到这里面存的时10 27 00 00那么是我们的想法错误了吗?当然不是

00000000000000000010011100010000这就是10000的二进制完整补码然后4个二进制位转为1个16进制位

文件操作函数详解_#include_28

那么10000的16进制序列也就是00002710又因为我们一般的计算机都是小端储存,那么

由低到高,低地址储存低位字节的数据,高地址储存高位字节的数据

那么计算机储存也就是10 27 00 00完美符合

这也就是说明了我们的说法是正确的。

那么我们现在再来看一下

二进制文件和文本文件的概念:

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

现在我们已经学会了文件的顺序读写,那么如果我们在读一个存有abcdef的文件当我们读到b之后还想在读一次b我们又该怎么办呢?这里我们要知道当我们通过fgetc读取到a之后在文件中也会有一个指针,读取一次之后文件里面的指针就会自动跳到下一个字符。这也是为什么当我们多次使用fgetc去读取一个文件(假设文件里面储存的值为abcdef)我们第一次会读到a,第二次会读到b以此类推,直到文件末尾。而在读到文件末尾之后系统会做一些操作而这些操作是什么,在下面讲解feof函数的时候我会详解。

这个时候我们就要使用文件的随机读写函数了。

首先是fseek函数

文件操作函数详解_打开文件_29

从图我们可以看到这个函数需要三个参数,第一个是文件流,第二个偏移量(这个偏移量是针对于起始位置也就是第三个参数),第三个参数也就是起始的位置从图里面我们能看到这里面只有三个可选项

第一个SEEK_SET:也就是文件起始位置,第二个SEEK_CUR也就是文件指针(这个文件指针和上面的文件指针指的是不同的事物,这个文件指针维护的是文件内部的数据,而上面的文件指针维护的是内存里的文件信息区)的当前位置,第三个选项也就是SEEK_END也就是文件末尾。

我们写一个代码来解释这个函数的作用:

#include<stdio.h>
int main()
{
	char name[20] = "zhangsan";
	FILE* pc = fopen("test.txt", "w");
	if (pc == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs(name, pc);
	fclose(pc);
	pc = NULL;
	return 0;
}//通过这个代码将zhang存到文件中去
#include<stdio.h>
int main()
{
	FILE* pc = fopen("test.txt", "r");
	if (pc == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(pc);
	printf("%c ", ch);//z
	ch = fgetc(pc);
	printf("%c ", ch);//h
	ch = fgetc(pc);
	printf("%c ", ch);//a
	ch = fgetc(pc);
	printf("%c ", ch);//n
	fclose(pc);
	pc = NULL;
	return 0;
}

文件操作函数详解_数据_30

现在通过上面的代码我们已经实现了将 z h a n 打印出来,此时文件里面的那个指针已经指向了g但是如果我想要下面一个要输出z那么就要使用fseek函数了,如果我们以最后的位置作为origin的话就要让文件指针偏移-4,而若是以当前的位置作为origin就要偏移-4额我这个例子不好请见谅,我们以首位置作为origin的话就要偏移0。

代码实现

#include<stdio.h>
int main()
{
	FILE* pc = fopen("test.txt", "r");
	if (pc == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(pc);
	printf("%c ", ch);//z
	ch = fgetc(pc);
	printf("%c ", ch);//h
	ch = fgetc(pc);
	printf("%c ", ch);//a
	ch = fgetc(pc);
	printf("%c ", ch);//n
	fseek(pc, 0, SEEK_SET);
	ch = fgetc(pc);
	printf("%c ", ch);
	fclose(pc);
	pc = NULL;
	return 0;
}

文件操作函数详解_数据_31

至于以末尾和文件指针当前位置作为origin的情况因为我这个例子很不好就不写了。

下面我们再来学习两个函数

ftell和rewind函数

第一个函数的功能就是返回文件指针相对于起始位置的偏移量

第二个函数的功能就是让文件指针的位置回到文件的起始位置

至于如何运用我这里就不实例写了。但将两个函数的详解放在这里,当然你也可以去https://legacy.cplusplus.com/reference/cstdio/ftell/?kw=ftell自己查询。

文件操作函数详解_数据_32

文件操作函数详解_数据_33

现在我们来学习最后一个函数。

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

而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

那么这个函数有什么用呢?

首先当我们在读取一个文本文件的时候我们怎么判断它真的是因为文件内容读取完毕了而结束,还是因为一些未知错误而导致的文件读取失败呢?

这个时候我们就要注意:

文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )

例如: fgetc 判断是否为 EOF . fgets 判断返回值是否为 NULL .

那么对于二进制文件呢?

我们就要使用fread函数了,当读取二进制文件时fread判断返回值是否小于实际要读的个数,若小于则是有未知错误导致了读取文件的错误。

我们通过下面的代码来了解怎么使用

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");//以读的方式打开test.txt的文件
    if(!fp) {//这个代码的意思就是如果fp接收到的不是一个空指针那么!fp就为假不执行下面的代码
    //如果fp返回的是空指针那么!fp就为真执行下面的代码
        perror("File opening failed");//打印造成错误的原因
        return EXIT_FAILURE;//这个返回的是一个1,后面的字母就是c语言自带的#define定义的一个1
   }
 //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
   { 
       putchar(c);//这里就是先将从fp中读取到的信息赋给c之后判断这一整个表达式是否是-1,EOF就是-1
       //如果不是-1则代表表达式为真打印c,若为一个-1则-1不等于-1表达式为假,
       //循环结束且不会执行循环内的语句
   }
//判断是什么原因结束的
    if (ferror(fp))//从图片看到如果读取结束的时候没有设置与流关联的错误指示器
    //那么这个函数就会返回0值代表并没有什么错误导致读取失败
    //否则在读取结束的时候若有错误那么就会设置与流关联的错误指示器
    //那么这个函数的返回值就为非0就会打印下面的字符串
        puts("I/O error when reading");
    else if (feof(fp))
    //若在上面的fetc函数最后是因为遇到了文件末尾而停止读取的那么这里feof函数就会返回
    //非0值否则返回的就是0值
        puts("End of file reached successfully");
    fclose(fp);
}

文件操作函数详解_打开文件_34

文件操作函数详解_数据_35

接下来我们来看对于二进制文件的读取我们又是怎么判读的

#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
    fclose(fp);
    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
    //这里的size_t也是一种数据类型
    //这里的ret_cod里面读取到的就是我们从文件读取到的数据个数,若成功读取到了1个数据就会返回1,两个
    //就会返回2
    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))//也有一种可能是文件中的数据少于我们估计的数量
       //那么在这里再次判断如果feof返回的是一个非0的值那么就代表是我们读取的数据太多了
          printf("Error reading test.bin: unexpected end of file\n");
       else if (ferror(fp)) {//如果这里返回的是非0值那么就代表读取的时候发生了错误导致了读取结束
           perror("Error reading test.bin");
       }
   }
    fclose(fp);
}

在了解完这些函数之后我们还要了解关于缓冲区的知识点:

我们先想一下为什么我们使用例如fscanf或是fputc,fputs之类的函数就能够将数据写入到文件中呢?

这是因为我们在使用这些函数的时候调用了操作系统的接口,通过这个接口才让我们能够将数据储存到文件中去,但是当我们在使用我们的计算机的时候很明显不只有我们这一个程序在运作,还有其它很多的程序那么我们如果隔一会就调用一次接口,那么这样就会拖慢操作系统的运行效率,为了解决这样的问题由此出现了缓冲区,当我们在需要向文件中储存数据的时候这些数据暂时会先储存在缓冲区中,等到缓冲区满了,再由操作系统一起将这些数据送到目的文件中去。

我们可以通过下面的代码来验证:

#include <stdio.h>
#include <windows.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区
	printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(10000);//程序延缓十秒运行
	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
	//这个函数的功能就是将现在缓冲区中的数据,先送去目的地
	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);
	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;
	return 0;
}

文件操作函数详解_#include_36

文件操作函数详解_#include_37

从运行截图上来看确实是如此:

那么我们总结一下在什么情况下缓冲区内的数据会被存到目标文件中呢?

  1. 缓冲区已满:当缓冲区已经充满数据时,再次使用写操作时就会导致缓冲区的数据被写入目标文件中。
  2. 调用 fflush 函数:fflush 函数可以使缓冲区的数据立即被写入目标文件中。例如,如果需要将输出立即写入文件中,则可以使用 fflush 函数。
  3. 关闭文件:当文件关闭时,缓冲区的数据会被写入目标文件中。这通常是程序正常结束时自动发生的,也可以通过调用 fclose 函数来手动关闭文件

由此我们就完整的学习完了文件操作函数

下面我们就来完成一个能储存信息的通讯录这里我就不详细讲了,只解释改变了哪些文件

头文件:

#include<stdio.h>
#define MAX_NAME 20
#define MAX_SEX 5
#define MAX_TEL 12
#define MAX_ADR 30
#define Add_Info 2
#define More 3
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>
typedef struct PerInfo//一个人的信息
{
	char name[MAX_NAME];//名字
	int age;//年龄
	char sex[MAX_SEX];//性别
	char tel[MAX_TEL];//电话
	char add[MAX_ADR];//地址
}PerInfo;
typedef struct COLST
{
	PerInfo* data;
	int sz;//表示通讯录到第几个了,即若要储存信息能找到
	//这个信息储存的位置。
	int contst;//表示当前通讯录的容量,默认为3,不过扩大空间即可
	//
}colst;
void Info_colst(colst* p);//初始化通讯录
void Add_colst(colst* p);//添加信息
void Show_colst(const colst* p);//打印现已储存的信息
void Del_colst(colst* p);//删除指定人信息
void SER_colst(const colst* p);//查找指定联系人
void Mod_colst(colst* p);//修改指定联系人
void Sort_colst(colst* p);//排序按照姓名或年龄
void All_Del(colst* p);//删除所有人的信息
void Free_Allspace(colst* p);//在退出通讯录后,free掉malloc开辟的空间
////对于这一个通讯录呢有一个问题那就是不能够储存我们上一次储存的任务信息
//那么我们今天就来解决这个问题
//很明显我们要解决这个问题,那么我们就要在用户完成功能选择退出通讯录之前添加一个函数
//这个函数的作用就是将此时内存中存的数据写入硬盘中
void Save_Contect_Data(colst* p);//保存数据文件

逻辑测试文件:

#include"动态通讯录.h"
void menu()
{
	printf("******************************\n");
	printf("*****1.Add******2.Del*********\n");
	printf("******3.Ser****4.Mod**********\n");
	printf("******5.Show  6.ALL_DEL  *****\n");
	printf("*****0.Exit   7.SORT**********\n");
	printf("******************************\n");
	printf("******************************\n");
}
enum option
{
	EXIT,
	ADD,
	DEL,
	SER,
	MOD,
	SHOW,
	ALL_DEL,
	SORT
};
int main()
{
	int input = 0;
	colst con;//这个结构体变量t就是一个通讯录
	//初始化结构体
	Info_colst(&con);
	do
	{
		menu();
		printf("请选择功能->");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			Add_colst(&con);
			break;
		case DEL:
			Del_colst(&con);
			break;
		case SER:
			SER_colst(&con);
			break;
		case MOD:
			Mod_colst(&con);
			break;
		case SHOW:
			Show_colst(&con);
			break;
		case ALL_DEL:
			All_Del(&con);
			break;
		case SORT:
			Sort_colst(&con);
			break;
		case EXIT:
			Save_Contect_Data(&con);//这个就是将将数据储存到文件中去
			Free_Allspace(&con);//释放空间
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误,请重新选择:\n");
		}
	} while (input);
	return 0;
}

函数实现:

#include"动态通讯录.h"
static int Is_Enough(colst* p)
{
	if (p->sz == p->contst)//如果当前有效数据个数等于了默认的数据数
	//那么添加就需要括容
	{
		p->data=(PerInfo*)realloc(p->data, (p->contst + Add_Info) * sizeof(PerInfo));
		if (p->data == NULL)
		{
			printf("Is_Eough中扩容失败,失败原因为:%s", strerror(errno));
			return 0;
		}
		p->contst += Add_Info;
		printf("扩容成功");
		printf("当前容量为%d\n", p->contst);
		return 1;
	}
	return 1;
}
void Loading_Message(colst* p)
{
	//打开文件
	FILE* pc = fopen("colstdata.txt", "rb");
	PerInfo tmp = { 0 };
	if (pc == NULL)
	{
		printf("通讯库还未储存过任何信息\n");
		return;
	}
	while (fread(&tmp, sizeof(PerInfo), 1, pc))
	{
		Is_Enough(p);
		p->data[p->sz] = tmp;
		p->sz++;
	}
	fclose(pc);
	pc = NULL;
}
void Info_colst(colst* p)//初始化通讯录
{
	assert(p);
	p->sz = 0;
	p->contst = More;
	p->data = (PerInfo*)malloc(sizeof(PerInfo) * More);
	if (p->data == NULL)
	{
		printf("Info_colst初始化失败,原因是:%s\n", strerror(errno));
		return NULL;
	}
	else
	{
		printf("初始化成功\n");
	}
	Loading_Message(p);
}
void Add_colst(colst* p)
{
	assert(p);
	if (1 == Is_Enough(p))
	{
			printf("请输入姓名:->");
			scanf("%s", p->data[p->sz].name);
			printf("请输入年龄->:");
			scanf("%d", &p->data[p->sz].age);
			printf("请输入性别->");
			scanf("%s", p->data[p->sz].sex);
			printf("请输入电话->");
			scanf("%s", p->data[p->sz].tel);
			printf("请输入地址->");
			scanf("%s", p->data[p->sz].add);
			p->sz++;
			printf("添加成功\n");
	}
	else
	{
		printf("通讯录增容失败,无法再次添加信息\n");
		return;
	}
}
void Show_colst(const colst* p)
{
	assert(p);
	printf("%-10s %-3s %-4s %-20s %-20s\n", "姓名", "年龄", "性别", "电话", "地址");
	for (int i = 0; i < p->sz; i++)
	{
		printf("%-10s %-3d %-4s %-20s %-20s\n", p->data[i].name, p->data[i].age, p->data[i].sex, p->data[i].tel, p->data[i].add);
	}
}
static int FindByName(colst* p, char na[])//加入了static让这个函数只能在这一个源文件
//使用其它源文件无法使用
{
	int count = -1;
	int i = 0;
	for (i = 0; i < p->sz; i++)
	{
		int ret = strcmp(p->data[i].name, na);
		if (ret == 0)
		{
			return i;
		}
	}
	if (count == -1)
	{
		return -1;
	}
}
void Del_colst(colst* p)
{
	assert(p);
	if (p->sz == 0)
	{
		printf("通讯录暂时没有任何人的信息无法删除\n");
		return;
	}
	printf("请输入要删除人的名字->");
	char na[MAX_NAME];
	scanf("%s", na);
	int count = FindByName(p, na);
	if (count == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
	for (int i = count; i < p->sz - 1; i++)
	{
		p->data[i] = p->data[i + 1];
	}
	p->sz--;
	printf("删除完成\n");
}
void SER_colst(const colst* p)
{
	char na[MAX_NAME];
	printf("请输入要查找人的姓名:\n");
	scanf("%s", na);
	int count = FindByName(p, na);
	if (count == -1)
	{
		printf("要查找的人,不存在\n");
		return;
	}
	printf("%-10s %-3s %-4s %-20s %-20s\n", "姓名", "年龄", "性别", "电话", "地址");
	printf("%-10s %-3d %-4s %-20s %-20s\n", p->data[count].name, p->data[count].age
		, p->data[count].sex, p->data[count].tel, p->data[count].add);
	printf("查找完毕\n");
}
void Mod_colst(colst* p)
{
	printf("请输入要修改人的姓名->");
	char na[MAX_NAME];
	scanf("%s", na);
	int count = FindByName(p, na);
	if (count == -1)
	{
		printf("需要修改的人不存在\n");
		return;
	}
	printf("请输入姓名:->");
	scanf("%s", p->data[count].name);
	printf("请输入年龄->:");
	scanf("%d", &p->data[count].age);
	printf("请输入性别->");
	scanf("%s", p->data[count].sex);
	printf("请输入电话->");
	scanf("%s", p->data[count].tel);
	printf("请输入地址->");
	scanf("%s", p->data[count].add);
	printf("修改完毕");
}
int Compare_By_Name(const void* p1, const void* p2)
{
	return strcmp(((PerInfo*)p2)->name, ((PerInfo*)p1)->name);
}
int Compare_By_Age(const void* e1, const void* e2)
{
	return (((PerInfo*)e1)->age - ((PerInfo*)e2)->age);
}
void Sort_colst(colst* p)
{
	assert(p);
	if (p->sz == 0)
	{
		printf("暂未有人被储存,无法排序\n");
		return;
	}
	printf("请选择使用年龄(0)还是名字(1)排序\n");
	int input = -1;
	scanf("%d", &input);
	if (input == 0)
	{
		qsort(p->data, p->sz, sizeof(PerInfo), Compare_By_Name);
	}
	else if (input == 1)
	{
		qsort(p->data, p->sz, sizeof(PerInfo), Compare_By_Age);
	}
	printf("排序完成\n");
}
void All_Del(colst* p)
{
	int input = -1;
	do
	{
		printf("请确定是否需要删除全部联系人的资料\n");
		printf("如果是请输入1取消则输入2\n");
		scanf("%d", &input);
		if (input == 2)
		{
			printf("取消删除\n");
			return;
		}
		else if (input == 1)
		{
				p->sz = 0;
				p->contst = More;
				p->data = (PerInfo*)malloc(sizeof(PerInfo) * More);
				if (p->data == NULL)
				{
					printf("Info_colst初始化失败,原因是:%s\n", strerror(errno));
					return NULL;
				}
				else
				{
					printf("初始化成功\n");
				}
			printf("删除成功\n");
			return;
		}
		printf("选择错误,请重新选择\n");
	} while (input);
}
void  Free_Allspace(colst* p)
{
	free(p->data);
	p->data = NULL;
	printf("堆区空间已清空\n");
}
void Save_Contect_Data(colst* p)
{
	assert(p);
	FILE* pc= fopen("colstdata.txt", "wb");
	if (p == NULL)
	{
		perror("fopen");
		return;
	}
	for (int i = 0; i < p->sz; i++)
	{
		fwrite(p->data+i, sizeof(PerInfo), 1,pc);
	}
	printf("数据储存完毕\n");
	fclose(pc);
	pc = NULL;
}

相比于之前的通讯录修改的就是添加了一个数据增加函数:Save_Contect_Data(&con)

以及修改了初始化通讯录的文件,增加了这个函数的功能让其能够读取文件的数据,并将其储存到内存中去。

希望这篇博客能对你有所帮助,如若有错误,请严厉指出我一定虚心改正。