文件操作—知识点小集结
- 啥是文件
- 文件的分类
- 文件名
- 文件类型
- 文件缓冲区
- 文件指针
- 文件的打开和关闭
- fopen
- fclose
- 文件的顺序读写
- fgets fputs 与 gets puts
- fseek
- feof 和 ferror
啥是文件
我们之前说过要实现通讯录的第三次升级,即文件版本的通讯录。那么为啥要有文件版本的呢?因为我们平时在使用通讯录这样的程序的时候,我们不能每次打开都是从头输入联系人吧。因此,这也是为什么我们需要将文件操作加入通讯录中,是为了让我们的程序不只是一次性的,也具有保存数据的功能。
好了,说了一下大概的用途,我们就来了解一下啥是文件吧
文件的话,广义的说,磁盘上的东西都可以成为文件
文件的分类
那么在我们的程序设计里面,我们将文件分成程序文件和数据文件
程序文件:包括源程序文件(如文件后缀名带.c的),目标文件(Window环境下后缀为.obj),可执行程序(Window环境下后缀为.exe)
数据文件:不是程序,而是程序运行中可能读取的数据,比如在程序运行中要从一些文件里读取数据,或者向一些文件里写入数据
文件名
一个文件要有唯一的标识让用户识别和使用
文件名:文件路径+文件名主干+文件名后缀
比如
对于test.c文件来说,F:\\编程\\c语言\\2021_3\\2021_3_24\\test 就是它的文件路径,test就是它的文件名主干,.c就是它的文件名后缀
这里简单了解一下就好
文件类型
这里需要注意,我们的文件从类型上来说可分为文本文件和二进制文件
二进制文件:数据本来是以二进制的形式存储在内存里面的,如果它不加转换后直接输出到外存,就叫二进制文件
文本文件:如果在外存中要求以ASCII码值进行存储,那么就需要在存储之前进行转换,转换之后以ASCII码字符形式存储的文件就叫文本文件
那么一个数据在内存中是如何存储的呢?
如果是字符,一律是用ASCII码方式存储,数字的话既可以用ASCII码字符形式存储,也可以用二进制存储
假如这里有整数1234,那当它输入进磁盘存储的时候�它既可以用四个字符’1’,‘2’,‘3’,‘4’来输入磁盘,也可以用二进制010011010010来输入磁盘。
具体用哪种方式就取决于我们用’w’还是’wb’(详见下文)。
文件缓冲区
啥是文件缓冲区呢?
ANSIC C标准(美国国家标准协会对C语言的发布的标准)会采用缓冲文件系统的形式来处理数据文件,所谓的缓冲文件系统就是系统会自动的在内存中为每一个正在使用的内存开辟一块“文件缓冲区”,从内存向磁盘(屏幕,输出流都一样)中输出的数据会先输送到输出缓冲区,等到缓冲区装满以后,再送到磁盘中。同理,从磁盘(也可以是从键盘,输入流都一样)中向内存输入数据时,也会先将数据送到输入缓冲区内,等到缓冲区装满后,再送入到内存中。
文件指针
了解我们上面的文件缓冲区以后,我们就可以继续来了解我们的文件指针,文件指针和我们之前学习到的指针也差不多,只不过它是指向一个文件的,但是我们所知道的文件是并不在程序内存中,而是在磁盘中,那么它是如何指向一个文件呢?
每一个文件在使用的时候都会在内存中开辟一个对应的文件信息区,文件信息区对于文件来说相当于函数传参的时候形参相当于实参的拷贝一样。如下图
这些文件信息是被存储在一个开辟的结构体中,如下图,在stdio.h中我们可以查看以下的文件类型声明。
文件的打开和关闭
明白了文件时被拷贝在内存中的时候,我们就可以更好的理解如何打开和关闭一个文件。
fopen
fopen 是C语言中打开一个文件的函数,其原型是:FILE fopen(const char firename,const char mode)。第一个参数是打开文件的名称,第二个参数是打开文件的方式。如果打开失败,那么就会返回一个空指针,因此在我们打开文件之后,我们需要对文件指针进行检查*。文件名很好理解,那么打开方式有哪些呢?可以参照下图并进行记忆。
注意:在测试中博主发现a+与ab+会建立新文件,同时依然会报错
直接让读者去记忆确实有点多,下面我给读者总结一些记忆的技巧和它们各自需要记住的特点
1.首先,r(read 读),w(write 写),a(add 追加)这三个一定要记住。
2.对于读来说,这是当你要从文件里面输入(也可以理解成提取)数据的时候使用,你用r打开一个文件的时候,文件本身并不会发生改变。
3.对于写来说,它既有一个剪切的功能,也就是你如果使用写打开一个文件,那么源文件里面的内容将会消失,因此对应的当你将数据写入文件的时候相当于在一个空文件里面写,写什么都是新定义的,与之前无关。
4.对于追加来说,它就不会毁坏源文件里面的内容,因为打开文件时返回的文件指针就指向文件的末尾。
5.了解了上面几点后对于b(binary)和+(增加了读和写功能),只需要对应的将文本文件换成二进制文件,以及功能变成读写兼有。
6.另外对于如果文件不存在的情况,要记住3点,1. r 及其衍生的模式均会报错 。 2. w 及其衍生的模式永远会建立一个新的对应文件。
3. a及其衍生的模式均会报错
虽然感觉上面的要点有点多,但是你要尝试理解着去记忆,那么要点会帮助你很好的记忆关于文件打开的各种模式及其对应的情况。
最后再总结一下,就是a和r不能在无文件的情况下对文件进行打开,均会报错,只有w适用于无文件的情况。
下面给一些简单的代码示例
int main(){FILE *pf = fopen("file.txt", "r");if (pf == NULL){printf("打印失败\\n");}fclose(pf);pf = NULL;}
fclose
有打开就有关闭,如果你只打开不关闭,就可能会造成下次文件的打不开。反正记住
打开文件后一定要记得关闭文件同时将文件指针置空
fclose的函数原型:int fclose( FILE stream );
参数的话很简单,就是你已经打开的文件指针,返回值的话,如果你已经成功关闭,那么将会返回0.否则出错就会返回EOF。
那么为什么要将文件指针置空呢?下面看个实验
到这里,我们已经可以知道了*,fclose语句并不能对指针的值进行置空,只是单纯释放了指针所指向的空间,而此时如果我们对指针解引用,我们仍能访问到指针所指向的空间,但此时这块空间已经被置空了,这就会形成一种非法访问的错误。
因此我们还要对指针的值进行置空。**
这时我们对指针进行置空后,我们就无法访问指针所指向的空间了,就能让我们的程序更加的安全。
文件的读写
注意一点很容易犯的错误:那就是读就读,写就写,你打开的读模式,你就使用输入函数,你打开的写模式,你就使用输出函数,不能搞混
文件的顺序读写
为啥叫文件的顺序读写?何为顺序?
不妨你先对这些函数进行了解,了解它们的功能之后我们再谈为什么称这些函数为文件的顺序读写。
注意到,在此目录下有很多函数,那么我们应该如何去了解,或者以何种的方式来了解呢?
结合之前的知识来记忆,在看完下面的了解后,也许你对之前的函数了解就更深了一个层次。
fgetc,fputc
fgetc fputc 与 getchar putchar
fgetc 和 fputc (参数和功能与getc putc 一样,二者可以相互替换)
我们先来整体了解一下它们的函数原型
fgetc: int fgetc( FILE *stream );//从文件中读取一个字符,
getchar: int getchar( void );//从标准输入(键盘)中读取一个字符
fputc: int fputc( int c, FILE *stream );//输出一个字符到一个文件中
putchar: int putchar( int c );//输出一个字符到标准输出(控制台)中
仔细观看它们两两之间的参数差异,我们可以发现带 f 的,即与文件有关的,它的后面总是会多一个文件输出流和文件输入流的文件指针,其实仔细想一想我们会发现,getchar和putchar也有,只不过它们默认输出到标准输出流(控制台)或者从标准输入流输入(键盘),只不过是省略了,相当于后者分别是前者的固定板。
那么它们的返回值呢?
全都相同,如果输入成功或者输出成功,那么全部都是返回你所输入或者输出的字符的ASCII码值,如果发生错误,那么就返回EOF
fgets.fputs
fgets fputs 与 gets puts
fgets: char *fgets( char *string, int n, FILE *stream );
//从stream中读取最多 n 个字符到string中存储,同时第n个字符被修改成’\\0’来结束一个字符串,相当于只读取n-1个字符。这样说有点抽象,我们来做个实验来了解一下吧。
如果更深入了解一点,我们做实验的时候会发现 ,当我们fgets中第二个参数为n,
如果stream中的字符数num<=n-1,那么就会成功读取num个字符,,然后在读取的字符串后面加上一个’\\0’来结束字符串。
如果stream中的字符数num>=n,那么就会成功读取n个字符,同时将最后一个字符改成’\\0’来结束字符串。
另外:fgets的返回值如果读取出错或者读取到文件末尾,那么将会返回一个NULL指针,至于如何来判断是读取出错返回的NULL还是读取完了返回的NULL,就要用到我们下面的函数来判断,请继续往下看吧
gets: char *gets( char *buffer );//从stdin(标准输入)中读取一个字符串到buffer中存储
//注意:gets和scanf都能读取字符串,那么它们有什么区别呢,读者可以移步到我的另一篇博客,以加深二者之间的了解
又是亿个小细节:如何让scanf像gets一样能读取带空格的字符串
fputs: int fputs( const char *string, FILE *stream );将string中的字符串输出到stream中。
//要注意两点,1.fputs输出到文件中时无换行,也就是它会接着你上一次的光标继续输出
2.fputs是通过检索字符’\\0’来结束字符串,要保证你的字符串末尾有字符’\\0’,但是’\\0’是不会被输入到文件中的
puts: int puts( const char *string );将string中的字符串输出到stdout(标准输出流)中
//要注意的时:puts输出到stdout中时自带换行
fscanf fprintf 、sscanf sprintf 与scanf printf
** scanf **: int scanf( const char *format [,argument]… );
相信读者如果学习到了文件操作,那么你对scanf已经很了解了,我就不讲scanf的用法了
那么对于scanf参数的返回值呢,记住,如果读取失败就返回EOF我们可以使用这个条件来实现循环读取一些数据,直到读完为止。
要注意一点:如何用scanf读取带空格的字符串,或者说修改scanf读取字符串的结束条件,请参看:又是亿个小细节:如何让scanf像gets一样能读取带空格的字符串
** printf ** : int printf( const char *format [, argument]… );
对于printf的返回值来说,如果成功就返回打印的字符数,如果出错就返回一个负数
** fscanf ** : int fscanf( FILE *stream, const char *format [, argument ]… );
//格式化的从文件中读取数据(输入)
要注意:fscanf不能在"w"的模式下使用,为什么?因为"w"的模式是’只写’的,只能写入不能读取
** fprintf ** : int fprintf( FILE *stream, const char *format [, argument ]…);
//格式化的向文件中放入数据(输出)
** sscanf** : int sscanf( const char *buffer, const char *format [, argument ] … );
//从一个字符串中格式化的读取数据(也就是将一个字符串转换成格式化的数据)
** sprintf**: int sprintf( char *buffer, const char *format [, argument] … );
//将格式化的数据输出到一个字符串中(也就是将格式化的数据转化成字符串)
到这我们对上述学习的函数,我们来做个小总结
1.我们要用联想记忆的方式去学习,例如通过scanf了解fscanf的操作
2.写一个记忆的小点,fgetc,fputc,fgets,fputs的文件流是最后一个参数,而fscanf,fprintf,sscanf,sprintf的文件流是第一个参数
3.比较特殊的还有一点,fgets中还有第二个参数,那就是你最多读取的字符数。
fread,fwrite
先来说几点fread和fwrite的几点与上述函数之间的不同之处
1.上述函数均适用于所有的输入流和输出流,而fread和fwrite只适合文件流
2.fread和fwrite只能在二进制打开的文件中使用,即使用"rb",“wb”,"ab"等带b的模式下。
3.因为是用二进制输入输出,我们可以看到fread和fwrite是用字节来作为单位进行输入输出的,
而不能像上面一样以一个字符串,一个字符啥的作为单位来输入输出
fread : size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
//这个函数的意思就是你从stream流中读取count个,每个大小为size的数据并存储到buffer中,要记住是从后往前
fwrite: size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
//与fread相反,这个函数的意思是从buffer中读取count个,每个大小为size的数据存储到文件stream中。
顺序读写的含义
顺序读写就是上述函数在调用一次之后文件指针就会发生偏移
举个例子
char arr[] = "avaksjd";FILE *pf = fopen("file.txt","w");fputc('a', pf);fputc('a', pf);fputs("bbbbb", pf);fputs("\\n", pf);fputs("aaaa",pf);fclose(pf);pf = NULL;
//输出结果是
aabbbbb
aaaa
你会发现当fputc第一次以后,再次使用,第二个a是跟在第二个a后面的,但我们并没有对pf这个指针进行自增这样的操作。
这也就是我们为什么称上述函数叫做文件的顺序读写,因为文件指针是会自动按顺序修改的。
那如何解决,或者说使用这种文件指针的移动呢?
请接着往下看。
文件的随机读写
先了解一个概念
偏移量:文件指针的位置相对于文件起始位置的偏移字节数
fseek
这是一个根据文件指针的位置和偏移量来定位文件指针的函数
函数原型:int fseek( FILE *stream, long offset, int origin );
第一个参数是文件指针,第二个参数是偏移量,第三个参数是一个参照点。
什么意思呢?
你根据参照点,然后来设置你的偏移量,最终让文件指针到达你想让它到的地方。
参照点共有以下三个:
SEEK_CUR
Current position of file pointer//当前文件指针所在的地方
SEEK_END
End of file//文件的末尾
SEEK_SET
Beginning of file//文件的开头
例如:
char arr[] = "avaksjd";FILE *pf = fopen("file.txt","w");fputc('a', pf);fputc('a', pf);fputs("bbbbb", pf);fputs("\\n", pf);fputs("aaaa",pf);fseek(pf,0,SEEK_SET);//这样你就将文件指针设置为指向文件开头了fclose(pf);pf = NULL;
这里如何定义,就看你自己的需求
ftell
顾名思义。ftell就是告诉你当前文件指针相对于文件起始位置的偏移量,
ftell: long ftell( FILE *stream );//返回偏移量
举个例子
char arr[] = "avaksjd";FILE *pf = fopen("file.txt","w");fputc('a', pf);fputc('a', pf);fputs("bbbbb", pf);fputs("\\n", pf);fputs("aaaa",pf);fseek(pf,-ftell(pf),SEEK_CUR);fputc('c', pf);fclose(pf);pf = NULL;
这段代码的输出结果是
cabbbbb
aaaa
这样文件指针就跳回了文件开头,修改了第一个字符
rewind
这个函数最简单,就是简单的让你的文件指针跳回文件开头
rewind: void rewind( FILE *stream );
文件读取结束判定
feof 和 ferror
feof:int feof( FILE *stream );
这是一个应用于当文件读取结束的时候,到底是发生了错误,还是读取到了文件末尾,这是一个判断函数
如果当前的文件指针是位于文件末尾,即正确读取到了文件末尾,那么feof将返回一个非零的数字
如果当前的文件指针不是位于文件末尾,那么feof将返回0
(可以记为非零即读完)
ferror
ferror:int ferror( FILE *stream );
如果文件中发生了错误,那么这个函数将不返回0,否则,将返回一个0(这个不能像feof一样判断文件是否位于文件末尾)
(可以记为零即正确)
注意:这两个函数不是用来判断文件是否结束,而是判断文件结束的方式
下面举个例子
FILE *pf = fopen("file.txt","r");char tmp;while (fscanf(pf, "%c", &tmp) != EOF){printf("%c\\n",tmp);}if (feof(pf)){printf("正确读取\\n");//非零即读完}else if (ferror(pf)){printf("错误读取\\n");//零即正确}fclose(pf);pf = NULL;
到这,我们C语言中的文件操作就告一段落,希望这篇文章对你有所帮助。如果你想和我一起学习进步就请关注我吧!在C语言专栏我接下来会更新如何利用文件操作知识来让我们的通讯录(之前博客中完成的小程序)变得更加可用。