创作不易,友友们给个三连吧!!
一、为什么我们需要使用文件
我们在写程序的时候,输入的数据是存储在电脑内存中的,如果程序退出内存回收,相应数据也就丢失了,等再次运行程序,就看不到上次输入的数据了,所以为了能够让我们的数据永久化保存,我们可以使用文件!
二、什么是文件
2.1 文件的分类
在磁盘(硬盘上)的文件就叫做文件,在程序设计中,文件一般分为两种:程序文件、数据文件。
程序文件包括源文件(.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
数据文件,因为文件的内容不一定是程序,也可能是程序运行时读写的数据,所以可以理解为程序运行需要从中读取数据的文件,或者输出内容的文件。
本文内容以数据文件为主!!
2.2 终端
终端:包括显示器、鼠标、键盘、耳机、麦克风、显示器和摄像头等等。 这些外围设备就被称为终端,负责向主机输入数据的就叫输入终端,比如鼠标、键盘、麦克风、摄像头,负责接收主机输出数据的设备就被称作输出终端,比如显示器、耳机。
以往在C语言程序编写中,我们处理数据的输入和输出都是以终端为对象的,即通过终端的键盘输入数据,并将运行结果显示在显示器上。
但是这样的方式显然不能达到永久保存数据的目的,所以我们需要把信息输出到磁盘的文件中,当我们需要的时候再通过磁盘中的文件将数据读取到内存中去使用!
2.3 文件名
我们为了将程序中输入的数据永久化保存起来,就需要用到文件,而每个文件都要自己的一个唯一的标识,用来方便用户的识别和引用。我们把这种文件标识叫做文件名。
文件名包括了3部分:文件路径+文件名主干+文件后缀
比如c:\code\test.txt
c:\code是文件路径,test是文件主干,.txt是文件后缀
一般想要知道某个文件的路径,即找到这个文件,右击属性,就可以找到他的位置
三、二进制文件和文本文件
根据数据的组织形式,数据文件又被区分为文本文件和二进制文件。
数据在内存中是以二进制形式存储的,所以如果不加以转换输出到磁盘文件中,那该文件就是二进制文件。如果要求以ASCII码的形式是存储在磁盘文件中,那么在存储前需要将二进制转换成ASCII形势存储,那么该文件就被成为文本文件。
数据在内存中的存储形式:字符一律以ASCII码形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
.txt后缀的文件一般为文本文件的格式,是我们肉眼可以看得懂的,而二进制文件是我们肉眼看不懂得,但是编译器可以读懂。
四、文件的打开和关闭
如何操作文件呢?操作文件的步骤其实和我们喝饮料一样,喝饮料的时候,先打开瓶盖,然后喝,最后关紧瓶盖。而我们想要操作文件,首先是打开文件,然后对文件进行读写操作,最后是关闭文件。
4.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了方便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
也就是说,流其实就是程序底层设计出来的一种东西,他存在于内存中的某一块区域,专门负责对接不同的外部设备,所以我们程序员并不需要关心我们的数据如何和外部设备建立联系,我们只需要关注流!!所以C程序针对⽂件、画面、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
4.2 标准流
在C程序中,我们通过键盘输入数据,并向屏幕中输出数据,其实都是通过流来完成。
因为在C语言程序启动的时候,默认打开了3个流:
• stdin – 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout – 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
• stderr – 标准错误流,大多数环境中输出到显示器界流。perror函数就是将错误的信息输出到标准输出流中
有了这三个流,我们就可以通过scanf/printf/perror函数来进行输入和输出操作。
而流本身作为程序底层开发出来的内容,存在于内存中的某一块区域,C语言中,通过FILE*的文件指针来维护流的各种操作。
stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为⽂件指针。
4.3 文件指针
缓冲⽂件系统中,关键的概念是“⽂件类型指针”,简称“⽂件指针”。
每一个被使用的文件都在内存中开辟了一个相应的文件区,专门用来存放文件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系 统声明的,取名FILE。
例如,VS2013编译环境提供的 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编译器的FLIE类型包含的内容不完全相同,但是大同小异,理论上来说,越高级的编译器,封装得会越完善,所以越不容易观察到这些细节。
也就是说,我们每当打开一个文件,系统就会根据文件的情况况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使⽤者不必关⼼细节。
⼀般都是通过⼀个FILE*的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。
如图,首先我们打开文件,其实就是向内存中申请了一块空间,该空间的类型是FILE类型,我们把它称为文本信息区,该文本信息区首先会根据原有的文本信息进行填充,之后再由操作者利用FILE*指针对这个文本信息区进行读或者写的操作,我们程序员不需要知道文件(data.txt)是如何和这个用结构体变量存储的文件信息区建立联系的,我们只需要知道我们可以用FILE*来操作文件,该文本信息区就相当于”流”,我们通过”流”和外部设备建立联系。
4.4 文件的打开和关闭
文件在读写之前要打开文件,使用结束后应关闭文件。
而我们在打开文件的同时,都会返回一个FILE*类型的指针,来帮助我们建立和该文件的联系,我们可以通过这个指针来进行读写的操作。
ANSIC 规定使⽤ fopen 函数来打开文件, fclose 来关闭文件。
4.4.1 fopen
const char * filename:传需要打开的文件的文件名const char * mode :传该文件的打开方式Open file:作用是打开文件
返回类型:FILE*指针,我们可以通过该指针完成对文件的读写操作(相当于通过该指针打开了一个文件流)
注:文件可能会打开失败,如果FILE*指针成功返回,说明打开成功,如果返回的是NULL,则打开失败!
4.4.2 fclose
FILE * stream:传维护该文件流的指针Close file:作用是关闭文件
4.4.3 打开模式
fopen函数中的参数mode表示文件的打开模式,下面都是文件的打开模式。
4.4.3.1 w和a的区别
w和a都可以写,但是如果我们打开的文件原来保存着一些数据,用w的话,会先清空掉文件的数据再进行写的操作,而用a则是直接在这些数据的后面进行追加
4.4.3.2 r+、w+、a+的区别
r+是覆盖写,即在原来文件数据的基础上从头开始覆盖写入
w+是清空写,即原来文件数据会被清空再开始写
a+是追加写,即在原本文件数据的基础上在后面追加写入
4.4.4 文件的存储路径
1.相对路径
int main(){FILE* pf;//打开文件pf = fopen("./../data.txt", "w");//桌面生成if (pf == NULL)//如果打开失败{perror("fopen");return 1;}//文件操作 //关闭文件fclose(pf);return 0;}
./ 当前目录
../ 上一级目录
/根目录
2.绝对路径
int main(){FILE* pf;//打开文件pf = fopen("C:\\Users\\lenovo\\Desktop\\data.txt", "w");//桌面生成if (pf == NULL)//如果打开失败{perror("fopen");return 1;}//文件操作 //关闭文件fclose(pf);return 0;}
五、文件的顺序读写
顺序读写函数介绍:
上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);所有输出流⼀ 般指适⽤于标准输出流和其他输出流(如⽂件输出流)。
5.1 fputc
int character :传需要输出的字符FILE * stream:传需要输出的流Write character to stream:作用是将字符写入流中
放置的过程其实跟光标有关,最开始光标是在最前面,输出a的时候光标移到b的后面了,所以再输出的b就是在a的后面,以此类推。
fpuc可适用于所有输出流,所以也可以使用标准输出流
5.2 fgetc
FILE * stream:传需要读取的流Get character from stream:作用是从流中获取字符
注:如果读取失败,会返回EOF;
原本文件里有abcd,我们调用了四次fgetc将他读取出来并打印在屏幕上。
与fputc一样,fgetc也适用于标准输入流,所以也可以从键盘读取
但是因为读取也是按照光标去逐个读取的,所以如果加上空格,就会有的数据读取不到
写一个代码,完成将data.txt文件的内容,拷贝一份生成data1.txt文件
int main(){//打开文件FILE* pfread = fopen("data.txt", "r");if (pfread == NULL)//如果打开失败{perror("fopen->data.txt");return 1;}FILE* pfwrite = fopen("data1.txt", "w");if (pfwrite == NULL)//如果打开失败{//第二次打开文件,如果打开失败,返回前必须把前一个文件关闭掉并置空fclose(pfread);pfread = NULL;perror("open->data1.txt");return 1;}//文件操作//数据的拷贝int ch = 0;while ((ch = fgetc(pfread)) != EOF)//循环读取直到读取不到{fputc(ch, pfwrite);}//循环结束后可以做到完全拷贝//关闭文件fclose(pfread);fclose(pfwrite);return 0;}
5.3 fputs
const char * str:传需要输出的字符串FILE * stream:传需要输出的流Write string to stream:作用是将字符串写入流中
5.4 fgets
char * str:传字符串存放的地址(一般是一个字符数组)int num:传从流中读取的最大字符串长度(由于字符串末尾有一个\0,所以默认其实最多读取num-1个)FILE * stream:传需要读取的流Get string from stream:作用是从流中获取字符串
我们可以看到,由于字符串的末尾要默认跟一个\0,所以最多只能读取num-1个到arr数组中
5.5 fprintf
FILE * stream:传需要输出的流const char * format:传格式化字符串,用于指定输出的格式。...:可变参数列表Write formatted data to stream:作用是将一块格式化的数据输出到流中
5.6 fscanf
FILE * stream:传需要读取的流const char * format:传格式化字符串,用于指定输入的格式。...:可变参数列表Read formatted data from stream:作用是从流中读取一块格式化的数据
注:%f打印时可以控制格式
5.7 fwrite
const void * ptr:传要写入的数据的指针size_t size:传每个数据的大小size_t count:传要写入数据项的数量FILE * stream:传需要输出的流Write block of data to stream:作用是将整块的数据输出到流中
因为是二进制形式,所以我们看不懂,但是编译器可以看懂,所以要使用fwrite就要配套使用fread!!
注:无论是什么文件,字符都是以ascii码值存储的,所以打印出来都一样!!
5.8 fread
const void * ptr:传要写入的数据的指针
size_t size:传每个数据的大小
size_t count:传要写入数据项的数量
FILE * stream:传需要输入的流
Read block of data to stream:作用是将整块的数据输入到流中
将fwrite输出到文件里的数据通过fread成功读取回来。
六、一些函数的对比
6.1 scanf/fscanf/sscanf
const char * s:用于读取的字符串const char * format:格式化的字符串,用于输入指定的格式 ...:可变参数列表Read formatted data from string:在字符串中读取一个格式化的数据
对比一下参数,共同点都是读取一个格式化的数据,不同的是scanf是默认的标准输入流,从键盘上读取,而fscanf是所有的标准输入流都可以,参数可以传文件流也可以跟scanf一样传stdin(标准输入流),而sscanf是从一个字符串中读取。
6.2 printf/fprintf/sprintf
const char * s:用于输出的字符串
const char * format:格式化的字符串,用于输出指定的格式
…:可变参数列表
Write formatted data from string:把一个格式化的数据转换成字符串
对比一下参数,共同点都是输出一个格式化的数据,不同的是printf是默认的标准输出流,输出到屏幕上,而fprintf是所有的标准输出流都可以,参数可以传文件流也可以跟printf一样传stdout(标准输入流),而sprintf是输出到一个字符串中。
6.3 总结
scanf:针对标准输入(键盘)的格式化输入函数
printf:针对标准输出(屏幕)的格式化输出函数
fscanf:针对所有输入流的格式化输入函数
fprintf:针对所有输出流的格式化输入函数
sscanf:从一个字符串中读取格式化的数据
sprintf:把一个格式化的数据转换成字符串
七、文件的随机读写
顺序读写,顾名思义就是按顺序读写,本质是根据光标的移动按顺序读写,而光标最开始就是在文件的最前面。
这里的随机,并不是类型随机数的那种随机,而是我们想在文件哪个位置读取就在哪个位置读取,想在文件哪个位置输出就在哪个位置输出,那么我们想要实现文件的随机读写,就要想办法去操控光标。
而由于流是通过FILE*指针维护的,所以本质上,也是通过操控指针来达到操控光标的目的。
7.1 fseek
FILE * stream:需要操作的流long int offset: 偏移量int origin:起始位置
这个origin可以传3个参数:
SEEK_SET:起始位置
SEEK_CUR:当前光标的位置
SEEK_END:文件的末尾
Reposition stream position indicator:作用是重新定位光标位置指示器(根据⽂件指针的位置和偏移量来定位⽂件指针)
7.2 ftell
FILE * stream:需要操作的流Get current position in stream:作用是获取流中的当前位置(返回⽂件指针相对于起始位置的偏移量)
利用fseek和ftell来判断文件有多少个字节(fseek操控光标到尾部,再由ftell返回长度)
7.3 rewind
FILE * stream:需要操作的流Set position of stream to the beginning:作用是将流的位置设置为开头(即让文件指针返回到文件的起始位置)
八、文件读取结束的判定
8.1 feof
FILE * stream:需要操作的流Check end-of-file indicator:作用是检查文件结束指示符(即在文件读取结束后,判断是否是因为遇到文件末尾而结束)
8.2 ferror
FILE * stream:需要操作的流Check error indicator:作用是检查错误指示灯(即检查操作目标文件的过程中是否出现了错误)
8.3 判断文本文件是否读取结束的方法
⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
• fgetc 判断是否为 EOF
• fgets 判断返回值是否为 NULL .
8.4 判断二进制文件是否读取结束的方法
⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。
例如:
• fread判断返回值是否小于实际要读的个数。
8.5 判断文件是因为什么原因结束的
8.5.1 文本文件
8.5.2 二进制文件
8.6 putchar
int character:要输出的字符Write character to stdout:作用是将字符写到屏幕上
8.7 getchar
Get character from stdin:作用是从键盘获取字符
8.8 puts
const char * str:要输出的字符串Write string to stdout:作用是将字符串输出到屏幕上
8.9 gets
char * str:存放读取字符串的数组Get string from stdin:作用是从键盘上获取字符串
九、文件缓冲区
ANSIC 标准采⽤“缓冲⽂件系统”处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为 程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。
这⾥可以得出⼀个结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。 如果不做,可能导致读写⽂件的问题(数据丢失)。