文件
我们程序运行时,产生的临时数据会存放在内存中,一旦程序退出,数据就会丢失。如果我们想让电脑断电后,数据依然得以保存,那就需要把数据存到硬盘中。
将数据写入文件,就可以把数据存到硬盘中
文件分类
在程序设计中,我们将文件分为:数据文件
与程序文件
。
程序文件
程序文件包括源文件.c
,目标文件.o
/ .obj
,可执行程序.exe
等
它们都是存储程序代码的文件,或者代码通过编译,链接后产生的文件。
数据文件
⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上⽂件。
两者关系
如上图所示,大部分情况下,都是程序文件读取数据文件中的数据,或者向数据文件写入数据。
文件名
文件名用于标识一个文件,其由多部分组成,保证可以通过计算机中的唯一文件名找到一个确定的文件。
文件名 = 文件路径 + 文件名主干 + 文件名后缀
示例:
c:\code\test.txt
文件路径:c:\code\
文件名主干:test
文件名后缀:.txt
二进制文件
根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件
文本文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂本⽂件
二进制文件
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存,就是⼆进制⽂件
比如对于一个十进制数字10000
,其有两种存储方式:
按照ASCII码值存储,最后存入了'1','0','0','0','0'五个字符,那么这就是一个文本文件。
按照一个int类型的数直接存储,这就是二进制文件;
流
概念
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念
程序员只需要向流中输入/提取数据,而与设备之间的数据交互,流会帮助我们完成。
标准流
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语⾔程序在启动的时候,默认打开了3个流:
stdin
- 标准输⼊流,在⼤多数的环境中从键盘输⼊。
stdout
- 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯。
stderr
- 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf
、printf
等函数就可以直接进⾏输⼊输出操作的
文件指针
当我们打开文件时,每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。
这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE
。我们可以通过操纵这个FILE
类型的结构体,来操控文件。
大部分情况下,我们可以得到一个指向该结构体的指针FILE*
,即文件指针,后续通过文件指针来操控文件,这也就是C语言操控文件的基本原理。
以上视图中,我们有三个指针pf1,pf2,pf3,它们都指向了一个FILE类型的文件信息区。而每个文件信息区都存储着一个文件的信息,我们后续就通过这三个指针来操控这三个文件。
文件的打开关闭
有了以上知识做铺垫,现在我们就可以在C语言中如何操控文件了。
文件在读写前,我们要现在程序中打开文件。
打开文件使用的函数是fopen
:
返回值:
返回值是一个
FILE*
的指针,也就是文件指针,后续通过这个指针操控文件
参数:
filename
:即需要打开的文件的文件名
mode
:打开的模式,需要用双引号
文件名很好理解,那么什么是打开模式?
打开模式分类如下:
模式 | 方式 | 含义 | 如果指定文件不存在 |
"r" |
只读 | 为了输⼊数据,打开⼀个已经存在的⽂本⽂件 | 出错 |
"w" |
只写 | 为了输出数据,打开⼀个⽂本⽂件,如果存在同名文件,则清空原先数据 | 创建一个新的文件 |
"a" |
追加 | 向⽂本⽂件尾添加数据 | 创建一个新的文件 |
"rb" |
只读 | 为了输⼊数据,打开⼀个⼆进制⽂件 | 出错 |
"wb" |
只写 | 为了输出数据,打开⼀个⼆进制⽂件,如果存在同名文件,则清空原先数据 | 创建一个新的文件 |
"ab" |
追加 | 向⼀个⼆进制⽂件尾添加数据 | 创建一个新的文件 |
"r+" |
读写 | 为了读和写,打开⼀个⽂本⽂件 | 出错 |
"w+" |
读写 | 为了读和写,建议⼀个新的⽂件,如果存在同名文件,则清空原先数据 | 创建一个新的文件 |
"a+" |
读写 | 打开⼀个⽂件,在⽂件尾进⾏读写 | 创建一个新的文件 |
"rb+" |
读写 | 为了读和写打开⼀个⼆进制⽂件 | 出错 |
"wb+" |
读写 | 为了读和写,新建⼀个新的⼆进制⽂件 | 创建一个新的文件 |
"ab+" |
读写 | 打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写,如果存在同名文件,则清空原先数据 | 创建一个新的文件 |
以“w”
形式打开:
FILE * pFile = fopen ("myfile.txt", "w");
该打开方式是以写形式打开,也就是说我们打开文件后,可以向文件内写入数据。其有两个特性:
- 如果打开的文件不存在,那么会创建这个文件
- 如果打开的文件存在,那么会清空这个文件,从头开始写入
以“r”
形式打开:
FILE * pFile = fopen ("myfile.txt", "r");
该打开方式是以读形式打开,也就是说我们打开文件后,只能读取文件中的数据。其有以下特性:
如果打开的文件不存在,那么会发生错误
对于文件名:
我们先前都是直接输入文件名的主干 + 后缀来打开文件的。这种方式只能打开与源文件在同一目录下的文件。如果目标文件与源文件不在同一目录下,那么就需要用路径来打开了。
比如这样:
FILE * pFile = fopen ("C:\\user\\cp\\Desktop\\myfile.txt", "r");
要注意的是,路径分隔符\
在C语言字符串中有转义的作用,需要用\\
来表示一个\
。
其它的打开方式,在表中已经描述过功能,不再详细讲解了。
文件关闭使用的函数则是fclose
:
其用法很简单,就是把指向文件的FILE*
指针传入即可。
FILE * pFile = fopen ("myfile.txt", "r"); fclose(pFile);
以上代码就完成了myfile.txt
文件的打开和关闭。
文件读写
现在我们知道了如何打开和关闭文件,那么打开文件后,我们就需要对文件进行读取数据和写入数据了。
文件读写分为顺序读写和随机读写。
顺序读写
fputc
功能:向文件写入一个字符
参数:
character
:要写入的字符
stream
:目标流的指针
示例:
FILE* pf = fopen("test.txt", "w"); fputc('q', pf);
以上代码就完成了向test.txt
文件中写入一个q
的操作。
fgetc
功能: 从文件中读取一个字符
参数:
stream
:目标流的指针
返回值:
当文件读取到末尾或者读取错误,返回EOF
示例:
FILE* pf = fopen("test.txt", "r"); char a = fputc(pf);
以上代码就完成了从test.txt
中读取一个字符的功能。
特性:
当多次读取同一个文件,会从上一次读取的位置开始读取
所以我们可以根据这个特性,读取到整个文件的内容:
FILE* pf = fopen("test.txt", "r"); char a = fputc(pf); while(a != EOF) { printf("%c", a); a = fputc(pf) }
只要a != EOF
,那么就会一直读取下去,而下一次读取同一文件,会从下一个字符开始读取,所以可以读取到整个文件。
fputs
功能:向文件中写入一行字符
参数:
str
:要写入的字符串
stream
:目标流的指针
特性:
该函数完成的是一行的写入,所以在字符串结束时会自动追加一个
\n
换行
示例:
FILE* pf = fopen("test.txt", "w"); fputs("hello", pf); fputs("world!", pf);
经过以上写入过程后,test.txt
内部存放数据如下:
hello world!
fgets
功能:读取一整行字符串
参数:
str
:读取后字符串存储的位置
num
:读取字符的数量
stream
:指向目标流的指针
注意:如果读取的字数不足num
,那么会中止读取
示例:
char arr[50] = { 0 }; FILE* pf = fopen("test.txt", "r"); fgets(arr, 20, pf);
以上代码就完成了读取test.txt
中一行的字符,且不超过20个,并存入arr
中
fprintf
fprintf
与先前的printf
非常相似,其实它们的用法也几乎是一致的。
我们看看printf
:
printf("%d %c %s", 1, 'w', "hello");
该函数用于格式化地向屏幕输出字符串,其操作的流是stdout
。
而fprintf
也用于格式化地输出字符串,但是可以自己指定输出到哪一个流。也就是其第一个参数指定的流。
示例:
FILE* pf = fopen("test.txt", "w"); fprintf(pf, "%d %c %s", 1, 'w', "hello");
可以看到,除了多了一个流,其剩余的操作和printf
完全一致。
fscanf
相似的,fscanf
也就是可以从指定的流中,格式化地读取数据。
示例:
FILE* pf = fopen("test.txt", "r"); fscanf(pf, "%d %c %s", &a, &b, &c);
以上代码就是从test.txt
文件中,按照int
,char
,字符串
的格式读取三个数据,存放到a
,b
,c
三个变量中。
sprintf
功能:得到格式化后的字符串
与printf
也非常相似,直接看用法:
char arr[100] = { 0 }; sprintf(arr, "%d %f %s", 5, 3.14, "hello");
此函数将格式化了字符串 "%d %f %s", 5, 3.14, "hello"
并将其存放到arr
中。
sscanf
功能:从格式化的字符串中提取数据
示例:
char* p = "5 3.140000 helloworld!"; sscanf(p, "%d %f %s", &a, &b, &c);
按照"%d %f %s"
的格式,从字符串中读取了三个变量,并存放到变量中,变量存储的值如下:
a = 5; b = 3.140000; c = "helloworld!";
文件随机读写
先前的所有函数,都是从文件的头部开始往后读取的,而文件也是可以从任意位置开始读写,这种读写叫做随机读写
。
fseek
参数:
stream
:目标文件指针
offset
:指针偏移量
origin
:开始偏移的位置
对于origin
,其有三个指定值:
SEEK_SET
:从头部开始偏移
SEEK_END
:从末尾开始偏移
SEEK_CUR
:从当前位置开始偏移
示例1:
FILE* pf = fopen("test.txt", "r"); fseek(pf, 5, SEEK_SET); char c = fgetc(pf);
以上代码中,先使用fseek
来改变文件指针指向的字符,SEEK_SET
说明从文件头部开始偏移了5
个字符。此时再使用fgetc
读取到的就是第5个字符了。
示例2:
FILE* pf = fopen("test.txt", "r"); for(int i = 0; i <10; i++) { fgetc(pf); } fseek(pf, -3, SEEK_CUR); fgetc(pf);
以上代码,先执行了10次fgetc
,此时文件指针指向第10个字符。接着使用了fseek
,由于第三个参数为SEEK_CUR
,所以会从当前字符也就是第10个字符开始偏移,偏移-3
个地址,此时指向第7个字符。再使用fgetc
读取到的就是第7个字符了。
ftell
功能:返回当前指针相对于起始地址的偏移量。
rewind
功能:让当前指针返回到起始位置
读取结束判定
feof
功能:当文件读取结束时,判断结束的原因是不是遇到文件末尾
返回值:
返回0:说明不是因为遇到文件末尾而结束
返回非0:说明是因为遇到文件末尾而结束
ferror
功能:当文件读取结束时,判断结束的原因是不是发生错误
返回值:
返回0:说明不是因为发生错误
返回非0:说明是因为发生了错误