文件操作详解

这期我们来了解文件操作

目录

1.为什么要使用文件操作?

2.什么是文件

 2.1程序文件

2.2数据文件

2.3文件名

3.文件的打开和关闭

3.1文件指针

 3.2文件的打开和关闭

4.文件的顺序读写

 4.1对比一组函数

5.文件的随机读写

5.1.fseek

5.2.ftell

 5.3.rewind

6. 文本文件和二进制文件

7.文件读取结束的判定

7.1 被错误使用的feof

 8.文件缓冲区


1.为什么要使用文件操作?

我们在之前写的通讯录时,有一个致命的问题,我们每次关闭程序后,再次运行程序,我们保存的联系人信息就消失了,而在我们实际使用的通讯录,是没有这个问题的,所以我们需要掌握文件操作,来解决这种问题,我们可以使用文件操作将数据保存到电脑磁盘上,这样才能让数据持久化

2.什么是文件

磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)

 2.1程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境
后缀为.exe)
文件操作详解_第1张图片

 比如上面的test.c就是文件文件操作详解_第2张图片

在我们的电脑上一定要把这个文件拓展名勾选,不然后续使用时会有一些麻烦 

2.2数据文件

假设我们写了一个test.c的程序,这个程序可以读取a文件里的信息,之后再把这些信息写入b文件,那我们所操作的a文件和b文件就是数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件

 我们以前的输入输出,比如我们使用scanf就是从键盘读取数据,printf就是输出到屏幕上,这些操作都是以终端为对象

2.3文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名

 

前面的 c:\code\ 是文件路径,test是文件名主干,.txt是文件后缀

3.文件的打开和关闭

在c语言里想打开或者关闭文件,我们要先了解文件指针

3.1文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE
我们要操作一个文件,首先要打开文件,然后进行读/写操作,比如我们打开一个test.txt文件,一旦打开这个文件,我们就会在内存中创建一个文件信息区,这个文件信息区和这个文件是相关联的,这个文件信息区里会记录这个文件的名字是什么,这个文件有多大之类,这个文件信息区是一个结构体变量

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结构的变量,这样使用起来更加方便

 文件操作详解_第3张图片

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

 文件操作详解_第4张图片

 3.2文件的打开和关闭

要想打开和关闭文件,我们要使用两个函数,一个是fopen,一个是fclose

文件操作详解_第5张图片

文件操作详解_第6张图片

对于fopen,他的第二个参数是mode,代表打开方式,有多种打开方式

文件操作详解_第7张图片

r是读,w是写,a是追加,r+是读或者更新,w+是写或者更新,a+是追加或者更新,fopen的返回类型为FILE*,当打开文件时,创建一个文件信息区,会将这个文件信息区的起始地址返回

我们来看个打开文件的例子

 当然,既然是打开文件并且返回一个指针,就会有打开失败的可能,打开失败会返回一个空指针

 文件操作详解_第8张图片

比如此时,错误信息就是这个文件不存在

文件的使用就和申请空间一样,我们使用后是需要关闭文件的

int main() {
	FILE* pf = fopen("C:\\code\\test.txt", "r");
	if (pf == NULL) {
		perror("fopen");
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

fclose也是有失败的可能,成功时返回0,失败时返回EOF

我们上面打开文件的方法叫做绝对路径打开,是从根目录开始打开文件,我们还可以使用相对路径来打开

文件操作详解_第9张图片

比如我们的test.txt文件在我们代码这个路径里,那我们可以不用写路径

 如果我们的test.txt在这个文件的上一层

文件操作详解_第10张图片

 我们可以这样打开

..的意思就是上一级目录 ,一个.是当前目录,两个.是上一级目录,比如文件在上一级的上一级,我们需要这样写

 相对路径的意思就是相对于代码所在的位置

fopen的打开方式是有很多种的

文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

比如w方式,如果文件存在,我们打开文件时会将文件里的内容清空,相当于创建一个新的文件

4.文件的顺序读写

我们打开文件后,要对文件进行读写,即读取文件里的数据和向文件内写入数据,进行读写操作,我们要使用新的函数

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件

我们先来看fgetc和fputc

文件操作详解_第11张图片文件操作详解_第12张图片

 fputc是写文件,第一个参数是我们要写入的字符(int是因为传入的是ASCII码),第二个参数是文件指针,我们来看个例子

 文件操作详解_第13张图片

 我们此时目录下面已经有了test.txt文件,并且是一个空的文件

文件操作详解_第14张图片

我们进行写文件的操作,然后再看看我们的test.txt

文件操作详解_第15张图片

 如果我们想写入a~z,我们可以使用循环

文件操作详解_第16张图片

文件操作详解_第17张图片

 fputc是一个一个的写,一个写完接一个,他叫做顺序读写的原因就是因为他是按照一个接一个的顺序,而不是随机位置写入

 我们再来看fgetc,他的返回值是我们读取到字符的ASCII码值

文件操作详解_第18张图片

 fgetc在第一次使用时会指向这个文件的第一个字符,使用完后他会自动将指向文件第一个字符的指针向后移动一位,所以会发生我们上面的现象

fgetc和fputc是针对字符来读写的,而且也是有可能读取失败的,比如fgetc,读取失败会返回EOF

接着我们来看另一组函数,fgets和fputs,他们是对文本行进行操作,是一行一行的读写

文件操作详解_第19张图片文件操作详解_第20张图片

 fputs是将str这个字符串写入到stream指向的文件里

 文件操作详解_第21张图片

文件操作详解_第22张图片

 如果我们想让两个字符串换行的话,我们写的时候加入\n即可

 fgets有三个参数,意思是从stream指向的文件里读取num(最多num)个字符放到str里,不够num会停止读取

 文件操作详解_第23张图片

 我们读取5个字符,为什么只有四个呢?这是因为还要存放\0,所以我们想读取5个时要写6,依此类推

文件操作详解_第24张图片

 我们文件内部本身是存在\n的,所以我们打印出加上\n后会多隔开一行

以上操作都是对于字符或者字符串,那如果想要对int类型,double类型进行操作,我们要使用新的函数,我们来看fprintf和fscanf,他们是格式化的读写

文件操作详解_第25张图片

文件操作详解_第26张图片

 我们发现这两个函数的参数里,后边都存在...,这叫做可变参数列表,我们看printf和scanf其实也是存在的

文件操作详解_第27张图片

文件操作详解_第28张图片

 这些函数是可以接收多个参数,比如一个两个三个都是可以的,我们来举个例子

文件操作详解_第29张图片

 这个知识我们了解即可,fprintf和printf使用方法是一样的,写完后只需在前面加上一个FILE*的指针即可

文件操作详解_第30张图片

文件操作详解_第31张图片

 fscanf也是同理

文件操作详解_第32张图片

 那么到了这里,可能会有人疑惑,为什么我们向文件写入数据用fprintf,而读取用fscanf呢?

我们来做个对比,我们平时写的程序,是存储在内存里的,我们用scanf是从键盘获取数据,我们用printf是打印到屏幕上

文件操作详解_第33张图片

而对于文件,我们的数据要放入到文件里,这个动作叫做输出/写,而我们要从文件里读取数据,这个叫做输入/读,我们要站在程序的内存角度来思考

文件操作详解_第34张图片

我们的输出是用 fputc/fputs/fprintf,我们的输入是用 fgetc/fgets/fscanf 

我们看到这六个函数的适用于都是所有输入/输出流,那流是什么呢?

我们可以把流想象成水流,对于我们的数据,我们可以把他们打印到屏幕上,写入到文件里,我们还可以上传到网络上,或者放入到硬盘网盘里,不管是屏幕还是文件或者网络,他们都是输出设备,我们要将数据发生到这些地方的方法肯定是不一样的,但是我们写c语言程序时,需要去了解这些所有的方法吗?这些输出设备的读写方法都要懂,那太麻烦了,所以C语言在二者之间抽象出一个流的概念,我们只需要写代码即可,至于这些数据是如何上传的,我们不需要关系,流会帮我们解决,这就大大减少了我们的学习成本,流的类型就是FILE定义和管理的

任何一个c语言程序运行时,会默认打开三个流,stdin,stdout,stderr,这三个流分别叫做标准输入(对应键盘),标准输出(对应屏幕),标准错误(对应屏幕),这三个流的类型都是FILE*,所以我们写程序时就可以直接使用scanf和printf这些,但是对于文件,我们c语言程序运行时并没有打开某一个文件,所以我们要先使用fopen打开一个文件,用一个指针接收,才能有一个流,有了这个流,我们才能进行读写文件

适用于所有输入/输出流的意思就是对于所有流都可以使用,比如我们的fgetc是可以从键盘获取数据的

文件操作详解_第35张图片

我们就可以使用这些函数来完成和scanf和printf一样的操作(第一个a是从键盘输入的,第二个是屏幕输出的),scanf和printf是只针对标准输入输出流的

文件操作详解_第36张图片

我们上面的这些函数操作文件后,我们打开文件时会发现文件里的信息和我们写入的是一样的,是肉眼能够看懂的,这是因为我们写入的都是文本信息,或者字符信息,接着我们来看二进制的输入输出,我们写入后是看不懂的,接下来我们来看fread和fwrite,这两个函数只适用于文件

文件操作详解_第37张图片

文件操作详解_第38张图片 

 我们看到这两个函数都有void*的指针,所以他们可以处理所有类型的数据

我们先看fwrite,第二个参数size是要被写入元素的大小,单位为字节,count是元素个数,我们来看例子

文件操作详解_第39张图片

文件操作详解_第40张图片 

此时我们发现,文本里的数据,除了zhangsan我们都不认识 ,zhangsan是因为以二进制和文本形式写进去是一样的,其他数据是不一样的,我们再通过fread读取试试

fread的参数及含义为从pf指向的文件里读取count个大小为size的数据放入到ptr里

文件操作详解_第41张图片

 4.1对比一组函数

scanf/fscanf/sscanf
printf/fprintf/sprintf

我们来对比这些函数 

scanf是从键盘上格式化读取数据,针对stdin流

printf是将数据输出到屏幕上,针对stdout流

fscanf是针对所有输入流的格式化输入函数,如stdin,文件等

fprintf是针对所有输出流的格式化输出函数,如stdout,文件等

我们来看看sscanf和sprintf

文件操作详解_第42张图片

文件操作详解_第43张图片 

sscanf是格式化的输入,从s这个字符串里读取数据,按照指定格式,放到后续的...所在的成员里

sprintf是格式化的输出,从后续的...成员里按照指定格式,放到str里

 我们来看例子

文件操作详解_第44张图片

 我们先看上面的sprintf,再看下面的sscanf文件操作详解_第45张图片

 是不是很有趣呢?

sscanf是从一个字符串中还原出一个格式化的数据,sprintf是把格式化的数据存放到一个字符串中

5.文件的随机读写

我们上面在写入文件和读取文件时,都是从开头向后一直到末尾,如果数据过多时我们想从中间位置读取数据就非常麻烦,所以我们需要随机读写,这个随机不是真的随机,而是我们指向哪里就到哪里,我们来看新的函数

5.1.fseek

根据文件指针的位置和偏移量来定位文件指针

 fseek的第一个参数是文件指针,第二个参数是偏移量,第三个参数是起始位置,其中第三个参数是有选项的,他有三个选项

文件操作详解_第46张图片

 SEEK_SET是从文件的起始位置算起,SEEK_CUR是从文件当前位置算起,SEEK_END是文件的末尾算起,我们来看例子

我首先在文件里写入abcdef这几个字符,接着用fgetc获取打印

文件操作详解_第47张图片

我们知道在文件打开时,指针会指向第一个元素,在读取一个后指针会自动向后走,读取c后,指针此时会指向d,我们从这里开始举例,打印出b,b在d前面2个位置,所以偏移量应该是-2

文件操作详解_第48张图片

 我们还可以从起始位置开始,向后偏移一个单位,同样可以得到b

文件操作详解_第49张图片

向前偏移,偏移量是负数,向后是正数 

不过,即使有了fseek,我们写代码还是很困难,因为我们有时候是不知道我们此时的偏移量是多少,所以我们需要一个新的函数

5.2.ftell

返回文件指针相对于起始位置的偏移量
文件操作详解_第50张图片

 比如我们指针指向c时,返回的就是2

文件操作详解_第51张图片

 5.3.rewind

让文件指针的位置回到文件的起始位置
我们也可以通过fseek让文件指针回到起始位置, 但那样太麻烦,所以用rewind函数就很方便

文件操作详解_第52张图片

文件操作详解_第53张图片 

 当然文件函数不止这些,大家有兴趣可以去了解一下

6. 文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)

 文件操作详解_第54张图片

文件操作详解_第55张图片

 我们来进行测试

int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}

文件操作详解_第56张图片

此时文件里这个方块一样的东西就是10000,我们把这个文件添加到vs里

 

 然后右击鼠标,选择打开方式,选择二进制编辑器即可

文件操作详解_第57张图片 

文件操作详解_第58张图片

 

此时显示的是16进制,但本质上是二进制(因为16进制方便展示) 我们的计算机目前是小端存储,10000写成二进制为00010000 00100111 00000000 00000000,不经过转换,直接存入文件就是这样的,我们上面vs里10270000前面的一堆0是地址,我们不用在意

7.文件读取结束的判定

我们在读取文件时,我们并不知道文件里有多少信息,应该读到什么时候结束呢?所以我们要知道文件读取结束的判定

7.1 被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
文件操作详解_第59张图片

 feof是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数

 当文件读取结束时,想要知道文件读取结束的原因

feof 返回真,说明是文件正常读取遇到结束符而结束

ferror 返回真,说明是文件在读取过程中出错而结束

文件操作详解_第60张图片

 8.文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的

文件操作详解_第61张图片 这里的硬盘指的是文件,我们程序里的数据,不是直接放到文件里,而是先放到缓冲区里,等缓冲区放满,或者主动刷新缓冲区才会放到硬盘里,读取的时候也是一样的,我们来用例子证明

int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}

文件操作详解_第62张图片

 当我们程序运行时,会输出提示信息,同时程序休眠10秒,此时我们打开文件,文件里什么都没有,10秒过后,会输出提示信息,提示我们打开文件,里面会写入内容

文件操作详解_第63张图片

 这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题

另外,缓冲区的大小是可以修改的

文件操作详解_第64张图片

 大家感兴趣可以去了解

以上即为本期的全部内容,希望大家可以有所收获

如有错误,还请指正 

你可能感兴趣的:(c语言,开发语言,c++,数据结构,算法)