目录
1、为什么使用文件
2、什么是文件
2-1 程序文件
2-2 数据文件
2-3 文件名
3、文件的打开和关闭
3-1 文件指针
4、文件的打开和关闭函数
4-1 fputc函数
4-2 fgetc函数
4-3 fgets和fputs函数
3-4 fprintf函数和fscanf函数
3-5 sprintf函数和sscanf函数
3-6 fread和fwrite函数
4、文件的随机读写
4-1 fseek函数
4-2 ftell函数
4-3 rewind函数
5、文件结束判定
5-1 feof
6、文件缓冲区
7、文本文件和二进制文件
我们首先来唠一唠为什么要有文件操作。
1、为什么使用文件
我们前面学习结构体时等,在写程序的时候,此时数据是存放在内存中,当程序退出的时候,程序中(比如通讯录)的数据自然就不存在了,等下次运行程序的时候,数据又得重新录入,如果使用这样的程序就很难受。
我们在想既然是程序就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
而使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2、什么是文件
磁盘上的文件就是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2-1 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2-2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
我们在这里主要讨论的是数据文件
我们之前所说的输入输出,都是以终端为对象的,即从终端的键盘输入数据,从终端上显示结果。
(就是我们说的terminal)
其实,我们有时候会把信息输入输出到磁盘上,当需要的时候再从磁盘上去读取到内存中进行使用。
这时候处理的就是磁盘上的文件。
2-3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
比如,在windows环境下:
这就是一个文件路径,在这个文件路径下的一个文件,在加上取后缀,就是其完整的文件标识。
比如以上路径\QQ 截图2021020311.png
为了方便起见,文件标识常被称为文件名
3、文件的打开和关闭
3-1 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。
这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE
就像下面这样:(在stdio.h中)
struct _iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
比如说,我们可以创建一个FILE类型的指针变量:
FILE* pf
这里所定义pf是一个指向FILE类型数据的指针变量。
可以使pf指向某个文件的文件信息区(是一个结构体变量)。
通过该文件信息区中的信息就能够访问该文件。
也就是说,通过文件指针变量能够找到与它关联的文件
画个图来理解一下:
4、文件的打开和关闭函数
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
这是一个好习惯。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定
我们用fopen来打开文件;
用fcolse来关闭文件;
在fopen的后面有一个mode参数,
它的意思是文件读写方式。
我们就简单了解几个即可。
另外,在打开了文件之后,我们还需要对其进行输入和输出。
像我们平时在屏幕上输入、控制台(屏幕)上输出是在标准输入流、输出流里输入和输出的。
我们对文件的输入和输出,也是借助一些函数来完成。
我们来看几个:
现在,我们对于其中的几个函数举几个例子:
4-1 fputc函数
我们来看其声明
其第一个参数为其要输出的字符,第二个参数为流参数(比如文件输出流、标准输出流)
标准输入流stdin、标准输出流stdout 和标准错误流stderr(FILE*)都是默认打开的流
4-2 fgetc函数
意为从一个流里面读入一个字符
如果都不到数据,就返回EOF
4-3 fgets和fputs函数
一行数据的输入和输出
对于fputs函数(写)
它的第一个参数是一个字符串的指针,第二个表示输入到哪个流里面。返回值是字符个数
而fgets函数(读)
它的第一个参数同样是字符串的指针,第二个为字符数,第三个为流;返回一个指针。
注意它是从一个流中读入,然后读到第一个string参数里
我们下面举一个例子:
#include <stdio.h> int main () { FILE * pFile; //打开文件 pFile = fopen ("myfile.txt","w"); //文件操作 if (pFile!=NULL) { fputs ("fopen example",pFile); //关闭文件 fclose (pFile); } return 0; }
上述的例子中,这个字符串就是“fopen example”,而其输入流是由pFile来维护的,输入到pFile所维护的输入流里,所以其给的参数是pFile
当我们ctrl + F5让代码运行起来的时候,我们到相应我文件路径下查看,发现确实多了一个文件myfile.txt,打开以后,发现其内容就是fopen example:
如果我们想再去测试一下fgets
来看:还是刚刚的文件,并且在刚刚的代码执行完毕的基础上:
3-4 fprintf函数和fscanf函数
我们同样的,还是先来看其定义:
对于fprintf来说,其相比于printf多了前面的一个流,所以,我们如果想要往某个文件里写入(即输出至文件流中),那就直接把其参数给它就可以了。
fscanf同理。
我们来举一组例子:
struct stu { int age; int sex; char name[20]; }; #include <stdio.h> int main() { struct stu pp = { 18,1,"laowu" }; FILE* pFile; //打开文件 pFile = fopen("myfile.txt", "w"); //文件操作 if (pFile != NULL) { fprintf(pFile, "%d %d %s", pp.age, pp.sex, pp.name); fclose(pFile); } return 0; }
我们ctrl + F5来让其运行一下:
然后从相应的文件路径下:
可以看到,其已经输出了我们想要其输出的内容。
同理,对于fscanf而言:
(如图所示)
这里的fscanf从文件流中将其数据输入到pp中
3-5 sprintf函数和sscanf函数
我们可以看出,对比sprintf和sscanf,它们只是相差了一个参数。
sprintf举例:
sscanf举例:
这里,我们先用sprintf将pp里面的内容全部输出到p里,形成一个字符串。
紧接着,我们用sscanf将pp里的内容全部输入到结构体op中。
然后将结构体op中的数据打印出来。
3-6 fread和fwrite函数
还是老样子,先看函数原型:
我们同样的道理,还是来举几个例子:
我们让其把结构体pp的内容写到文件中:
#include <stdio.h> struct stu { int age; int sex; char name[20]; }; int main() { struct stu pp = { 10,1,"zhangsan"}; char p[20] = { 0 }; FILE* pf = fopen("myfile.txt", "wb"); struct stu op = { 0 }; fwrite(&pp, sizeof(struct stu), 1, pf); fclose(pf); pf = NULL; return 0; }
注:(上文的“w”应为"wb”)
程序执行完后,我们打开myfile.txt文件,发现:
我们似乎并不能看懂。原因很简单,其是以二进制的形式存储的。
fwrite的第一个参数是从哪里写入的地址;
第二个参数是要写入元素的大小;
第三个参数是要写入多少个元素;
第四个参数是要写到哪里;
返回值是写入了多少个元素。
那编译器能不能看懂呢?
应该是能的。
这个时候,我们就引出了另外一个函数fread
我们在刚刚的基础上再次执行这样的代码:
注:(上文的“r”应为"rb”)
#include <stdio.h> struct stu { int age; int sex; char name[20]; }; int main() { struct stu pp = { 10,1,"zhangsan"}; char p[20] = { 0 }; FILE* pf = fopen("myfile.txt", "rb"); struct stu op = { 0 }; fread(&op, sizeof(struct stu), 1, pf); printf("%d %d %s", op.age, op.sex, op.name); fclose(pf); pf = NULL; return 0; }
这时候发现文件中的内容已经被读进标准输出流中。(并且就是我们刚刚写进去的)
说明编译器是能够看懂的。
那么fread函数也就是这么用的。
第一个参数是要写入(输出)的地址;
第二个参数是要写入元素的大小;
第三个参数是要写入几个元素;
第四个参数是说需要从哪里写入。
返回值是读取了多少个元素。
4、文件的随机读写
4-1 fseek函数
第一个参数stream就是往哪个地方操作;(即操作对象)
第二个参数是指指针偏移量。
第三个参数是指起始位置(即相对偏移的起始位置)其有如下三个选项:
我们再来举个例子:
如上图所示,这里的Q就输出到了B和D的中间。
4-2 ftell函数
这个函数就是返回一个值,该值 就是文件指针当前位置相对于其实位置的偏移量。
比如:
#include <stdio.h> struct stu { int age; int sex; char name[20]; }; int main() { struct stu pp = { 10,1,"zhangsan"}; char p[20] = { 0 }; FILE* pf = fopen("myfile.txt", "w"); fputs("ABCDE", pf); printf("%ld\n", ftell(pf)); fseek(pf, -3, SEEK_CUR); printf("%ld\n", ftell(pf)); fputc('Q',pf); pf = NULL; return 0; }
4-3 rewind函数
让文件指针回到起始的位置。
这个如果上面的理解的话,就已经很简单了,就不再举例了。
5、文件结束判定
5-1 feof
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
判断文本文件是否结束
就直接判断其是否返回EOF(如果用fgetc读取)或者NULL(用fgets读取)
二进制文件的读取结束判断
判断返回值是否小于实际要读的个数。
if(ferror(...)) {...} else if(feof(...)) {...}
6、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
有这么一个关系:
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
7、文本文件和二进制文件
我们简单来提一下:
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
二进制文件:数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。
文本文件:以ASCII字符的形式存储的文件就是文本文件。
好啦,本节的内容就到此为止啦~~~