你将收获:
进一步理解缓冲区,以及引申出getchar(),scanf("%c",&c)
接收数据时,易踩坑的地方,并对其解释。
w+,r+,a+
打开文件后如何正确使用读、写函数。
还有其它收获…
文件
- 为什么存在
- 什么是文件
- 文件的唯一标识符
- 缓冲文件系统
- 文件指针
- 为什么存在文件缓冲区呢?
- 文件的打开和关闭
- 打开文件
- 关闭文件
- 文件的顺序读写
- 读写函数
- fgetc
- fputc
- fgets
- fputs
- fprintf
- fscanf
- sprintf,fprintf,printf,sscanf,fscanf,scanf的比较
- fwrite
- fread
- 文件的随机读写
- fseek
- ftell
- rewind
- 对于 r+,w+,a+的正确使用
- 易错的feof
- ferror
- clearerr
- 学完了还等什么,还不操作一下你文件版本的通讯录
学完了文件操作得熟练熟练,拿通讯录试试手吧~
文件版通讯录
为什么存在
在以往写的程序中,运行程序后,这些数据都是在内存中,当程序运行结束,这些数据也随之消失。有什么方法可以使数据长久存在呢?一起走进文件的世界。
文件的存在的意义是让数据永久性的存储下来,当然永久性的存储不只有文件,还有数据库,数据库又是另一个高级的东西了。
什么是文件
根据功能分类,文件分为程序文件和数据文件。在写c语言程序时,运行前后产生文件的就是程序文件如
.c
为后缀的源程序文件,.obj
为后缀的目标文件,.exe
为后缀的,可执行程序。
数据文件:文件内容并不一定是程序,而是存放一些数据,这些数据可以被程序读和写。用一个例子来解释下,比如在写通讯录时,已经存一个文件,这个文件中存放了一些数据:张三的个人信息
在计算机语言中,读是指输入,写是指输出。
给定一个情境:在写通讯录小项目时,我需要知道文件中已经存在哪些联系人,并且呢我还需要对这些联系人的信息修改。 这里存在两个问题,怎么知道存在哪些联系人,怎么对这些数据进行修改。
首先,是不是需要将文件中的数据加载到程序中,我们才能进行下一步操作。那怎么加载就能存到程序里呢?我们只要将这些数据存到变量中,程序运行后,这些变量就会在内存中,那么数据也就完成了加载,这一过程就是读。写又怎么体现呢?已经将数据加载到程序中了,我现在想知道有哪些联系人,怎么才能知道呢,是不是要把这些数据打印出来,打印到屏幕上,我就能知道存了哪些联系人,这一过程就是写的过程。
暂且先到这里,下文我将继续以通讯录为例,让你学会这些简单的操作。
根据数据的组织形式,数据文件被称为文本文件或者二进制文件
二进制文件:打开文件是乱码的就是二进制文件,人看不懂,计算机看得懂就可以,类似于这种。
文本文件:打开文件你认识的一般都是文本文件。
二进制和文本文件浅浅的了解一下就可以了。
文件的唯一标识符
文件的唯一标识符也叫文件名是文件所存放的路径。
文件名包括三个部分:文件路径、文件名主干、文件后缀
相对路径和绝对路径的概念放到文件操作函数中解释,有个具体的例子会形象很多。
注意事项:
文件名并不一定要包含文件后缀。
文件名不能包含一些符号。
缓冲文件系统
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块文件缓冲区。
文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。先抛出两个概念,待会连起来解释。
在程序中如何去描述文件呢?是否还记得结构体呢。在结构体这篇博客中深度讲了自定义类型。
结构体存在就是用来描述复杂对象的。文件是一个复杂对象,在C语言中也确实是用一个结构体来描述文件的,描述文件的路径、名称、状态等。不同编译器下,结构体中的成员可能不一样。
VS2019中,文件的结构体封装的很好,找不到有效的信息。
这是在VS2010中的
以上你只要了解是用一个结构体来描述文件的即可,不用太在意结构体中每个成员的含义。
我们已经知道了文件是一个结构体,那用什么去维护这个结构体呢?在创建一个?不现时,文件是通过文件指针来维护的。通过文件指针和一些文件类函数就可以对文件进行读写操作了。
缓冲文件系统这里我们要深入理解文件缓冲区
文件指针没什么好说的,就是一个指针指向的是一个结构体。文件的结构体类型为:
FILE
,文件指针的类型为FILE*
,可能你还觉得有些抽象,等下讲到函数的时候,你就知道用起来贼容易。文件信息区就是指文件结构体内部成员汇集的信息。
最主要的还是文件缓冲区,引用linux中一个句话,一切皆文件,c中把文件称流。不管是哪种语言的编译器,编译器都会默认打开三个流(文件)标准输入流,标准输出流,标准错误流,标准输入流—键盘之类的,标准输出流–显示器之类的,标准错误流–通过全局变量
errno
(记录错误码),和一些函数来控制strerror、perror
在这篇博客中有strerror、errno的相关介绍
为什么讲这个呢?不知道你是否还记得刚入门时使用
getchar(),scanf("%c",&c)
老是细节出错呢?我们可以把那三个标准流的称呼为文件,这样和下面讲的刚好连起来了。
既然标准输入流是一个文件,那是不是需要一个文件缓冲区呢?当然需要,这个缓冲区叫输入缓冲区。我们在键盘上的输入的数据都是先存放在输入缓冲区中的,当缓冲区存满了,或者你按了回车,或者其它条件,都可以触发一次输送。怎么个输送法呢?就是将输入缓冲区中的内容存到内存中去,即存到变量中。
下面浅浅的举一个例子:目的是输入两个数,一个是整形一个是字符型,在输出到屏幕上。但是在键盘上按了123回车,程序就结束了。
要弄明白这些,你还得知道
%c
输入,和%s
输入的不同。%c
接收字符,也包括'\n'
(回车),%s
是不会接收'\n'
和空格的。
这只是一个简单的例子,如果在平时不注重细节的话,可能你都不知道出什么错。
为什么存在文件缓冲区呢?
你可以想象一个场景:你当老师,一个学生每隔一分钟就会来问你一个问题。这时候你就不能处理其他人的问题,只能处理这个学生的问题,是不是效率极低呢?你就会对这个学生说让它过30分钟来问一次,这样你有空间的时间处理其它的事情了。
在计算机中也是如此。当文件系统接收到
read
或者write
的请求时,会向磁盘驱动程序发出指令,磁盘驱动程序接收了指令,又会向硬盘发出指令,在通过一系列的操作,就完成了一次读或者写。如果没有缓冲区存数据,一个数据一个数据的接收,一个数据一个数据的传送,你可以想象这个过程是有多么的繁琐,造成的后果就是效率极低。文件缓冲区的存在就非常有必要了。
文件的打开和关闭
打开文件关闭文件,和喝饮料一样,需要喝,要打开瓶盖,不喝了就把瓶盖盖上。
打开、关闭文件需要配套使用,至于具体原因大体是这样的:还记得刚刚说的文件缓冲区吗,向文件读写数据都是先存到缓冲区中,触发条件后会输送一次,这个关闭文件的操作,也是一个触发条件。有时候可能你少了关闭文件的操作,数据可能会丢失。
打开文件
返回值:返回一个文件指针。
参数1:文件名
参数2:打开方式
如果打开文件失败会返回一个空指针,因此一般都需要进行一个判断,打开文件是否成功。
FILE* p =fopen("contact.txt","w");
if(NULL == p)
{
perror("fopen::");//错误提示
return 1;
}
FILE* p1 =fopen("D:\\Ccode\\contact2.txt","w");
if(NULL == p1)
{
perror("fopen::");//错误提示
return 1;
}
通过文件指针
p
来维护contact.txt
文件,通过p1
维护contact2.txt
文件。
绝对路径和相对路径在这就能很好的理解了。
相对路径是指以当前文件资源所在的目录为参照基础,链接到目标文件资源(或文件夹)的路径。
绝对路径指带域名的文件的完整路径。
以
"w"
的方式去打开文件如果文件不存在,那么会生成一个新文件,如果以"r"
的方式去打开文件,文件不存在会报错。下面有张图已经全部汇总了。
新的文件生成在哪呢?根据你在函数中写的文件名来看。p
指针,指向的文件是在相对路径下的,这个参照文件是指你正在写代码的这个文件(test.c
)的路径下新建一个文件
p1
指向的文件是绝对路径的,从磁盘到具体的文件夹下。至于为什么要\\
两个,因为\
是一个转义字符,要让它普通,得再来个转义字符\
,颇有点负负得正的意思。
一个小细节,类型都是
char*
的,是一个字符类型的指针,指向的是一个常量字符串,所以需要""
(双引号)。
运行程序后
打开方式:
当然还没结束,这只是开始,需要结合读写函数,才能更深入理解读,写,读写,这些是什么意思,稍后还会深入讲。
关闭文件
参数:文件指针。传一个文件指针,关闭的是这个文件指针所指向的文件。
fclose(p);
fclose(p1);
通过打开和关闭文件的操作,相信你应该可以浅浅的了解了如何通过指针来管理文件,再看几个读写函数的例子,你就能明白了。
文件的顺序读写
读写函数
所有输入输出流,也包括标准输入流(
stdin
)、标准输出流(stdout
)标准输入流指从键盘输入,标准输出流是指打印到屏幕上。stdin、stdout
类型是文件指针FILE*
另外要说明一点的是函数中给出的是
stream
,这个是流的英文,为了方便理解我都会说成文件。
fgetc
作用:从文件中读取一个字符读取失败返回EOF,读取成功就会返回读取到的字符。
读取失败:如果到了文件的结尾或遇到读错误,将返回EOF
通过一个变量接收这个返回值存入内存,或直接输出。
EOF
是什么?是end of file
–>文件结束标志,默认用-1
表示EOF
。
FILE* stream
:文件指针,该指针指向要被操作的文件
以这个函数为例实现标准输入
示例:对文件中的字符连续读取。
fgetc
根据上文提供的图片,可知是以"r"
的方式打开文件的。测试的时候可以先在相对路路径下创建好文件,进行读操作。如果没有文件,那么会报错。
先在文件中存了一些数据,连续读取数据,通过函数返回值来控制循环条件。
注意:文件的打开方式要和读写函数配对使用
仅仅知道这些当然还是不行的,我们还需要理解内部的机制。它为什么能够读取到每个字符呢?通过读写函数对文件进行操作的时候,会存在一个文件指针,它指向文件内部的数据位置,之前所说的文件指针是不同的。
每次调用
fget()
之后,这个文件指针会向后偏移一位,直到文件末尾。
fputc
作用:将
character
以字符的形式写到文件中,标识符位置向前移动。
文件内部的文件指针指向哪,就从哪开始写入。
返回值:发生错误返回EOF
,没有发生错误返回写入的字符的ASCII码值
int character
:要写入的字符
FILE* stream
:文件指针,该指针指向要被操作的文件
最后一句话也很好理解,标识符位置就是刚刚说的文件指针,如果这个文件指针指向向文件的起始位置
0
,写入一个字符,写到0
处,这个指针会向后偏移,偏移到1
处。
如果这个文件指针指向向文件的位置是1
处,写入一个字符,写到1
处,这个指针会向后偏移,偏移到2
处。
以这个函数为例,实现标准输出
示例:向文件中写入26个英文字母。这个时候是写,打开方式需要改变,以
w
的方式开打文件。
来看一个现象:一开始文件中是有数据的。
调试:执行完以只写的形式打开文件后,内部数据还会在吗?
不会在了。由此可以得出结论:以只写的形式去打开文件,可理解为每次都重新生成一个新的同名文件。
注意:文件的打开方式要和读写函数配对使用
还有一点需要注意的,
fgetc、fputc
操作的都是字符,以ASCll码值为对应关系。
通过上面两个例子,相信你应该能很好的理解文件指针是怎么来管理文件的了,就是这么简单。函数参数为文件指针,就可以对文件进行读写等操作。
fgets
作用:从文件中读取一行字符,存到
str
所指向的字符串中,当读取 (a-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char* str
:str
是指针用来接收读取的字符串,更准确的说str
是数组名。
int a
:读取的个数
FILE* stream
:文件指针,该指针指向要被操作的文件
返回值:
如果读取成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。
为什么读取
a-1
个字符就会结束呢?因为字符串的结束标志是'\0'
,'\0'
需要占一个空间。
为什么说
str
只能是数组名
示例:对读取
a-1
个字符进行验证
修改文件内容:
注意:文件的打开方式要和读写函数配对使用
memset
对字符串进行初始化操作,这篇博客有memset等相关内存函数的介绍,以及模拟实现
示例2:连续读取多行的数据,可以根据返回值来进行多次读取,也可以通过变量
i
来控制读取多少行。
更改文件信息
整个运行过程是这样的,每次循环读取
a-1
个字符,即读取1
个字符,第一次循环str
中存的是i\0
,打印i
,这个过程中str
指向的空间是没有改变的,第二次进入循环数据又会从str[0]
开始存入i\0
。一直重复,当读取到换行符\n
时,str
中存的是\n\0
,打印的结果就是换行,一直这样重复直到文件结束。
在使用的过程中要注意的是:
str
指向的空间,必须是大于等于a的
fputs
作用:把
str
指向空间中的字符串写到文件中。
const char* str
:str
是指针指向一个字符串,或者是数组名(数组的为char类型的,因为数组名本身就是个地址)
FILE* stream
:文件指针,该指针指向要被操作的文件
返回值:该函数返回一个非负值,如果发生错误则返回 EOF
注意:文件的打开方式要和读写函数配对使用
注意str
学好下面两个函数,通讯录的两个操作也就学会了----通讯录文件中的数据加载到程序中,和将程序中的数据保存到通讯录文中。
fprintf
作用:将内存中的数据以格式化的形式写到文件中。
和平时用的printf
一样,只多了一个参数—文件指针。
返回值:如果写入成功,则返回写入的字符总数,否则返回一个负数。
示例:将张三的个人信息填写到文件中
注意:文件的打开方式要和读写函数配对使用
fscanf
作用:将文件中的数据格式化输入,即将数据加载到内存中。
使用方法scanf
一样,也是多了一个参数----文件指针。
fscanf
遇到空格和换行时结束,注意空格时也结束。这与fgets
有区别,fgets遇到空格不结束。
返回值:读取成功返回读入的参数的个数,失败返回EOF(-1)。
示例:将张三的信息加载到程序中
注意:文件的打开方式要和读写函数配对使用
多次的读写通过循环去控制,在之前的博客通讯录中,介绍了单链表实现,还有顺序表实现的。不同的存储结构,实现循环的方式是不同的,有兴趣的可以看看。
sprintf,fprintf,printf,sscanf,fscanf,scanf的比较
这两个
sscnanf、sprintf
又是什么呢?看到前缀s
,可以推测一下是不是和字符串有关呢?
确实和字符串有关,sscanf
将字符串中的数据输入到内存中,sprintf
将内存中的数据输出到字符串中。
和fprintf、fscanf
差不多,文件指针改为字符类型的指针。
在使用
sprintf
时,字符数组需要足够大的空间。不可以使用字符指针,和上面讲的str
一样
汇总:
fwrite
作用:把
ptr
指向的数组中的数据写到文件中 。
ptr
是指向要被写入的元素数组的指针。
size
是要被写入的每个元素的大小,以字节为单位。
count
是元素的个数,每个元素的大小为 size 字节。
stream
是文件指针,指向一个文件
ptr
是类型是void*
,因为这里操作都是以字节为单位,通过个数来控制写入的数据。
示例:
存入4个整形
注意:文件的打开方式要和读写函数配对使用
不要奇怪为什么文件里是个乱码,因为
fwrite
会把数据以二进制的形式写到文件中,计算机看的懂就阔以。
fread
作用:将文件中的数据读取到
ptr
指向的数组中。
ptr
指向一个数组,这个数组最小有 size*count 字节的内存空间。
size
是要读取的每个元素的大小,以字节为单位。
count
是元素的个数,每个元素的大小为 size 字节。
stream
是文件指针,指向一个文件
这里
ptr
是void*
,原因和上面一样。
示例:将上面
fwrite
的数据在读取到内存中。
注意:文件的打开方式要和读写函数配对使用
看到这,你以为就掌握了吗,还没有呢,重点的还没讲完,需要学完下面的函数才能去了解
w+
、r+
、a+
方式打开时如何正确操作文件。
文件的随机读写
fseek
还记的上述讲的一个文件指针吗,指向文件数据中的文件指针。
作用:给定一个origin
位置,向前(后)偏移offset
个位置
FILE* stream
:文件指针指向要操作的文件
offset
:以origin
为起始位置,偏移offset
个位置
origin
:由库函数提供选择,有三个选择分别为文件开头,当前位置,文件末尾。
这里也有坑需要避过。
示例:先向文件写入abcdef
,操作fseek
函数。
ftell
作用:计算当前文件指针(指向文件内部数据的指针)相对于文件开头的偏移量。
返回值:相对于文件开头的偏移量
rewind
作用:使内部文件指针,重新指向文件开头
对于 r+,w+,a+的正确使用
讲到这里,可以开始去了解
a+,w+,r+
的使用了。
对于带+
的都是可以进行读和写操作,即输入函数和输出函数可以连用,无+
只能执行对应操作的函数,比如以w
方式打开文件,只能用输入函数,以r
方式打开文件,只能用输出函数。
原先文件存在
abcd
以
r
的方式打开文件,进行写操作,但并没有任何效果。
以
r
的方式打开文件,进行写操作和读操作,没有报错。读操作的函数起作用,写操作的函数不起作用。
以
r+
方式打开文件
上面的问题都和文件指针的指向有关。
对于其它函数也是如此。进行读或者写时,要注意文件指针的位置到底在哪,其它函数可能直接报错了,
fputs,fgets
就是这样。
对于读写函数同时使用,需要注意文件指针的位置,才能保证不出错。C语言中的写函数是不会对文件原数据进行覆盖的,如果想对原数据进行覆盖需要使用
write
函数。
易错的feof
为什么说是易错呢?先来了解
feof
作用:是检测文件结束符,如果文件结束,则返回非0值,否则返回0
文件结束符是EOF
,EOF
的值是-1
很多人误把
feof
用来检测文件是否出错。这不合理,我们可以来看看为什么。
文件正常读取数据,并没有发生错误,返回的也是0,文件出错,返回的也会是0,存在二义性,所以不可用feof来判断文件是否出错。
ferror
判断文件错误需要用
ferror
作用:检测文件是否出错。
如果ferror
返回值为0(假),表示未出错。如果返回一个非零值,表示出错。
还需要在介绍一个函数和
ferror
配套使用的clearerr
clearerr
作用:使文件错误标志和文件结束标志置为0
.假设在调用一个输入输出函数时出现了错误,ferror
函数值为一个非零值。在调用clearerr(fp)
后,ferror(fp)
的值变为0。(feof(fp)
同样是这样)
另外,只要出现错误标志、文件结束标志,就一直保留,直到对同一文件调用clearerr
函数或rewind
函数,或任何一个输入输出函数。
一般可以这样用