路漫漫其修远兮,吾将上下而求索。
在博主前一篇的文章中实现了通讯录的功能(PS:下一篇,如果没看到那么便还在创作中……),当通讯录运行起来的时候,可以为向通讯录之中添加联系人、删除联系人、查找联系人……,此时”联系人“的相关数据是存放在内存中的,而倘若没有将程序中的数据写入文件,那么当此程序退出的时候,在程序中输入的数据自然也就不在了,再次运行此程序的时候需要重新输入数据;显然,没有文件的通讯录失去了其”灵魂“;
在实现通讯率功能的时候,我们想将在程序中输入”联系人“的数据保存下来,只有我们自己选择删除数据的时候,数据才会不在,即当退出程序的时候,输入的”联系人“的信息仍然存在;这便会涉及数据持久化的问题;
让数据持久化的方式有:将数据存入磁盘文件、数据库等方式;
此处使用文件便可以将数据存放到磁盘文件之中,从而做到数据的持久化;
注:数据库只是封装了一层,其最终底层的数据还是会保存到某些文件中去的;
磁盘(硬盘)上的文件是文件
但是在程序设计之中,我们谈的文件一般有两种:程序文件、数据文件(从文件功能的角度来分类)
程序文件包括源文件(后缀为 .c)、目标文件(windows环境下后缀为 .obj)、可执行程序(windows 环境下后缀为 .exe)
数据文件中的内容不一定是程序,而是程序中运行时读写的数据;比如程序运行需要从文本读取数据的文件、或者输出的文件;
例如我们写了一个test.c 文件,当要运行test.c 文件的时候便会将数据写入B文件中,或者从A文件中读取数据;而此时,数据需要将程序中的数据写入文件中去或者从文件中获取数据到程序中,那么此时的A文件与B文件便是数据文件;如下图所示:
实际在前面学习的过程中,我们也接触到过输入输出,只不过是将键盘上的数据输入到内存中,或者将内存中的数据输出到屏幕上;
一个文件要有唯一的标识,以便用户识别和引用;
但是在生活中,你可能会发现,在不同路径下可以存在相同名称的文件,故而想要使得一个文件的文件名唯一,那必然是需要加上文件路径的;
即文件标识:文件路径 + 文件主干名 + 文件后缀
例如 : c:\code\test.txt
而为了方便起见,又将文件标识(文件标识:文件路径 + 文件主干名 + 文件后缀)称之为文件名;
根据文件的组织形式,数据文件被分为文本文件或者二进制文件;
注:字符一律都是以ASCII的形式进行存储的,而数值型的数据既可以用ASCII的形式存储又可以使用二进制的形式进行存储;
已知存在一个文件,倘若我们想使用该文件,那么我们改正么做呢?
想必你肯定听过将大象装进冰箱需要三步;即第一步,打开冰箱门;第二步,将大象装进冰箱;第三步,关闭冰箱门;
同理,使用文件,也有三步,第一步,打开文件;第二步,操作文件;第三步,关闭文件;
缓冲文件系统中,关键概念就是“文件类型指针”,简称“文件指针”;
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件的状态以及文件当前的位置等);这些信息保存在一个结构体变量之中;而该机构体类型是由系统声明的,取名为FILE
平时我们在写代码的时候,引用的头文件也是文件;
下段代码便是 VS编译器中,stdlio.h 文件中对于结构体类型FILE的声明;
struct _iobuf
{
char* _ptr; // 文件输入的下一个位置
int _cnt; // 当前缓冲区的相对位置
char* _base; // 基础位置(文件的起始位置)
int _flag; // 文件状态标志
int _file; // 文件的有效性验证
int _charbuf; // 检查缓冲区状况(若无缓冲区则不读取)
int _bufsiz; // 缓冲区大小
char* _tmpfname; // 临时文件名
} ;
typedef struct _iobuf FILE;
当然,在不同的编译器中其对于FILE 的声明也不同,但是大致是相同的;
每当打开一个文件,操作系统便会根据文件的情况自动创建一个文件信息区,即FILE类型的结构体变量,并自动填充其中的信息;
你有可能会有疑问,文件信息中也是有数据的,想要存放必定是要占用内存空间的,那么文件信息区究竟是存放在内存中是如何被维护的呢?
FILE* pf ;// 文件指针变量
定义的pf 是一个指向文件信息区的指针变量,而我们通过文件信息区便又可以访问该文件,即通过文件指针变量我们便能找到该指针变量对应的文件;
如上图所示,你若想要打开位于磁盘上的文件A,那么既可以直接在磁盘上打开该文件;
倘若你想在程序中打开在硬盘中的文件,那么便会通过文件指针变量找到该文件;
从上文知识中我们大体能感觉到倘若要在程序中打开文件就得利用FILE*的指针,那么可能便会猜想使用库函数打开文件定然也会与FILE*的指针变量有关;
我们再回顾一下,想要操作文件有三步:打开文件、操作文件、关闭文件;
ASCIN 规定使用fopen 来打开文件,使用fclose 来关闭文件;
注:fopen 的第二个参数 const char* mode;
文件使用方式 | 含义 | 如果该指定文件不存在 |
"r"(只读) | 为了将文本中的数据输入到内存中(输入数据),打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了将内存中的数据输出到文本二文件中(输出数据),而打开一个文本文件 | 创建一个新的文件 |
"a"(追加) | 将内存中的数据追加到文本文件的尾端,而打开一个文本文件 | 创建一个新的文件 |
"rb" (二进制读) | 为将二进制文件中的二进制数据输入到内存中,而打开一个二进制文件 | 出错 |
"wb" (二进制写) | 为了将内存中的二进制数据输出到二进制文件中,而打开的一个二进制文件 | 创建一个新的文件 |
"ab" (二进制追加) | 将内存中的二进制数据追加到二进制文件的尾端,而打开的一个二进制文件 | 创建一个新的文件 |
"r+"(读写) | 为了将文本中的数据输入到内存中(输入数据),将内存中的数据输出到文本二文件中(输出数据),而打开一个文本文件 | 出错 |
"w+"(读写) | 为了将文本中的数据输入到内存中(输入数据),将内存中的数据输出到文本二文件中(输出数据),而建立一个新的文本文件 | 创建一个新的文件 |
"a+"(读写) | 为了在此文件的尾端进行读和写,而打开一个文本文件 | 创建一个新的文件 |
"rb+"(读写) | 为了将文本中的二进制数据输入到内存中(输入数据),将内存中的二进制数据输出到文本二文件中(输出数据),而打开一个二进制文件 | 出错 |
"wb+" (读写) | 为了将文本中的二进制数据输入到内存中(输入数据),将内存中的二进制数据输出到文本二文件中(输出数据),而建立一个新的二进制文件 | 创建一个新的文件 |
"ab+" (读写) | 为了在此文件的尾端进行读和写,而打开一个二进制文件 | 创建一个新的文件 |
如若你用读的形式打开文件便就不能写,同样的,你若用写的形式打开便就只能写而不能去读;
注:以写、追加的形式打开文件,如若未找到该文件便会自己创建一个文件;而倘若以读的形式打开文件,如若没有该文件便会报错;
例2:以读的形式打开在桌面上的文件
打开文件的图解流程:
打开文件与关闭文件的代码如下:
注:在关闭文件之后要将该指针置空, 避免野指针;
文件在打开之后,不再使用时需要将文件关闭,原因在于:
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输入流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输入流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入函数 | fwrite | 文件 |
二进制输出函数 | fread | 文件 |
例如,我们在使用scanf 与 printf 会接触到标准输出流以及标准输入流,其具体作用由下图所示:
标准流有三个,分别为:标准输入流(stdin),标准输出流(stdout),标准错误流(stderr);
而标准流 stdin、stdout 、stderr 都是指向文件信息区的FILE*类型的指针;任何一个C程序运行起来均会默认将这三个流打开,故而在使用用printf、puts、putchar、scanf、getchar 等,不需要像操作文件那样,在对文件进行操作的时候还得打开文件(比如你若想从键盘上获取数据或者将数据打印到屏幕上去,并没有要求你利用库函数来打开键盘或者打开屏幕,取而代之的是只要你使用相应输入、输出的函数指明所要传递的数据即可即可);
注:此处讨论的输入、输出均是以 内存 作为主体,而其面临的对象非常多样,可以是键盘、屏幕(显示器)、硬盘、软盘、U盘、光盘等;而流,就像一个“数据缓冲区”;
操作如下:
在test.txt 文件中查看数据的输出效果如下:
注:函数fputc 是将内存中的数据写入文件中,故而在使用fopen 函数的时候应该以写的形式 即利用指令"w" 打开;
由于fputc 适用于所有输出流,同样,我们可以利用fputc 将字符输出到标准输出流--> 屏幕:
此函数的使用如下图所示:(因为在上例中使用 函数fputc在文件中放入了a~z 的字符 )
现test.txt 文件中的内容如下:
注:因为函数fgetc 是将文件中的数据读入到内存中,故而在使用函数 fopen 打开文件要以读的形式打开,即利用指令 "r"
由于fgetc 适用于所有输入流,那么便可以实现利用fgetc 从标注输入流(键盘)中获取字符,使用如下图:
使用如下:
注:每一次以"w"的形式打开文件,便会将文件中的原数据给清除以放入新数据;
由于fputs 适用于所有输出流,那么便也可以使用 fputs 将文本行输出到屏幕上,使用如下图所示:
此时文件test.txt 中的内容为下图所示:
使用函数fgets 如下:
由于fgets 适用于所有输入流,那么便可以实现利用fgetc 从标注输入流(键盘)中获取字符,使用如下图:
使用如下图:
在该程序下打开test.txt 文件:
由于fprintf 使用于所有的输出流,那么便也可以使用 fprintf 将格式化数据输出到屏幕上,使用如下图所示:
文件test.txt 中存放的数据如下图:
使用的代码如下:
#include<stdio.h>
struct Peo
{
char name[10];
int age;
}s;
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//操作文件
int ret = fscanf(pf, "%s %d", s.name, &s.age);
printf("%s %d\n", s.name, s.age);
printf("%d\n", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码运行结果如下:
由于fscanf 适用于所有输入流,那么便可以实现利用fscanf 从标注输入流(键盘)中获取格式化数据,使用如下图:
接下来的两个函数 fwrite 与 fread 只能适用于文件,并且在打开文件只能以二进制的形式打开,即利用指令"wb","rb"; b -> binary 二进制 ;
二进制写入与文本的写入有什么区别?
使用的例子如下图所示:
打开 test.txt 文件来看一下:
为什么我们看不懂这些字符?
因为在test.txt 中存储的是二进制的数据,但是我们查看文本中的内容时是以文本的形式打开该文件;
以二进制的形式来打开此文件:
此时文件test.txt 中存储的是数据如下图:
使用案例如下图所示:
看了上文,你会发现,当我们利用fgetc 读取文件中给的字符时,程序每一此启动,fgetc 均只能读到第一个字符;多写几个 fgetc 才能读到后面的字符;
倘若文件test.txt 中的数据如下:
使用一次fgetc 读取文件test.txt 中的字符:
第一次运行:
第二次运行:
为什么每次运行的结果一样呢?
利用多个fgetc 读取文件test.txt 中的字符:
为什么多次使用fgetc 能往后获取字符呢?
倘若你想要不按照顺序读取字符,下面的函数便可以实现此功能;
当前文件 test.txt 中的数据如下图:
利用fseek 与 fgetc 读取偏移起始位置2、4的字符,代码与输出结果如下:
使用例子如下:
当文件读取结束的时候,可能是因为遇到错误而停下,也有可能是因为遇到文件结尾而结束;
文件结束的标志:
如何判断呢?
此处便会利用到两个函数: ferror 与 feof
注:ferror 与 feof 通常是一起使用的
所要读取的文件test.txt 中的内容:
使用例子代码如下:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//将数据写入文件
char ch[15] = "chongqing";
size_t sz = strlen(ch) + 1;
fwrite(ch, 1, sz, pf);
//关闭文件
fclose(pf);
pf=NULL;
//在文件中读取数据
//打开文件
pf = fopen("test.txt", "rb");
size_t ret_code = fread(ch, 1, sz, pf);
//判断文件读取结束的原因
if (ret_code == sz)
{
puts("Array read successfully ,contents:");
printf("%s\n", ch);
}
else
{
if (ferror(pf))
printf("Array reading test.txt : unexpected end of file\n");
else if (feof(pf))
printf("End of file reached successfully\n");
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码运行结果如下:
ANSIC 标准采用“缓冲文件系统”处理的数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用地文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,当此缓冲区被装满的时候操作系统会将这些数据一起送到磁盘上;如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,此内存缓冲区被放满之后,操作系统才会将缓冲区中的数据送到程序数据区;其中,缓冲区的大小根据C编译系统决定的;
数据的输入输出并未都是直接操作的,而是先让如数据缓冲区之中;
缓冲区的大小取决于编译器;对于不同的编译器其缓冲区地设定不同;
缓冲区还分为无缓冲区、行缓冲区、全缓冲区……(当然,缓冲区的大小也是可以被设置地,有一些库函数专门用来设置缓冲区的大小)
证明缓冲区确实存在:
思考:我们利用输入函数(例如:fputc),再让操作系统延迟操作,我们趁这段延迟的时间打开文件,看文件中是否有数据;倘若没有,则证明有缓冲区;反之则无;
所要使用的相关函数:
注:会利用到fflush 的原因是 fclose 关闭文件时会刷新缓冲区;
代码如下:
#include<stdio.h>
#include<windows.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("perror");
return 1;
}
//操作文件
fputc('a', pf);
printf("睡眠10s,已经开始写数据了\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);
Sleep(10000);
printf("再睡眠10s \n");
Sleep(10000);
//关闭文件
fclose(pf);//fclose 关闭文件也会刷新缓冲区
pf = NULL;
return 0;
}
第一次10s 休眠:
第二次10s 休眠:
从上述代码中,可以证明缓冲区的存在,但是在不同的编译器上此文件缓冲区的设置是有所差异的;
在此处可以得到一个结论,因为有缓冲区的存在,需要做刷新缓冲区或者在文件操作时关闭文件才能确保数据能完全地输入或者输出;这点便可以加深之前的理解,为什么在打开文件操作完文件之后要记得关闭文件而避免数据的丢失;
1、有关输入的库函数:
2、有关输出的库函数:
3、想要判断文件读取结束的原因是什么会利用到两个函数: ferror 与 feof
4、流,FILE* 类型的指针便称为"流";针对文件、键盘、显示器(屏幕)、网络、U盘、软盘、光盘、打印机等外部设备上的数据的读写都是通过 '流' 来进行的;
5、数据的输入输出并未都是直接操作的,而是先让如数据缓冲区之中;缓冲区的存在是为了提高操作系统的效率
因篇幅问题不能全部显示,请点此查看更多更全内容