文章目录
一.为什么使用文件?
我们在写程序时,我们输入的数据在程序运行结束后将不会被记录保存,那么这时我们可以使用C语言中的文件操作将我们所需要的数据永久保存在磁盘上,做到数据持久化,以便于下次我们不必再重复输入输入数据,可以直接在文件中读取数据。
二.什么是文件?
持有数据的文件系统对象,能被写入或读取或二者皆可。文件拥有名称和属性,属性之一是文件类型,程序设计中我们一般(按文件的功能)将文件分为两类:
1.程序文件
文件的内容可以是程序,包括:源程序文件(后缀为.c
)
目标文件(windows环境后缀为.obj
)
可执行程序(windows环境后缀为.exe
)
2.数据文件
文件的内容可以是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者是输出内容的文件。
那么,如何将程序中的数据输出到文件中,如何将文件中的数据输入到内存中?
注意:在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
而现在我们要做的是:把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
根据数据的组织形式,数据文件被称为文本文件或者二进制文件
文本文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。(如整数10000,需要以ASCII码输出到磁盘上,则在磁盘中的存储形式就是10000).
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),以二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
字符数据本身在内存中就经过了编码,所以无论是二进制还是文本形式都是一样的,而对于非字符数据来说要进行ASCII或者unicode编码。
二进制文件
把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,即数据在内存中以二进制得到形式存储,不加转换的输出到外存,就是二进制文件。
用两种不同方式模拟计算机存储10000和10这两个数据:
从图中我们可以看出,从存储方式来说,文件在磁盘上的存储方式都是二进制形式,所以,文本文件其实也应该算二进制文件,区别就是二进制读写是将内存里面的数据直接读写入文本中,而文本是将数据先转换成了字符串再写入到文本中。
当以二进制形式存储整数10000:
int main(){ FILE* pf=fopen("text.txt","wb");//打开 if(pf==NULL){ perror("fopen"); return 1; } int a=10000; fwrite(&a,sizeof(int),1,pf);//存入整数10000 fclose(pf);//关闭 pf=NULL; return 0; }
存入后使用记事本打开此文件会出现乱码(记事本看不懂二进制文件),那我们想看懂二进制文件该怎么办呢?
VS就可以帮助我们读取二进制文件:
将二进制文件添加到编译器中使用二进制编译器的打开方式打开:
可以看到10000这个数据在二进制中存储为10270000(前面00000000是地址不需要在意)。10270000是将二进制数据转换为十六进制并以小端存储方式显示的。
参考博文文本文件和二进制文件的差异和区别_请简述文本文件和二进制文件的区别-CSDN博客
3.文件名
命名一个文件的字符串。容许字符,大小写区别,最大长度以及被禁止名称是实现定义的。名称.
(点)与..
(双点)在库层次拥有特殊含义。
文件名包含3部分:文件路径+文件名主干+文件后缀,例如:c:\code\test.txt
三.文件的打开和关闭
1.文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件所在的当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE
FILE*pf;//文件指针变量
定义pf
是一个指向FILE类型数据的指针变量。可以使pf
指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能找到与它关联的文件。
2.文件操作——打开和关闭
//打开文件
FILE*fopen(const char* filename,const char* mode);
//filename为文件名,mode为打开方式
//关闭文件
int fclose(FILE* stream);
实例:
int main(){
FILE* fp=NULL;
fp=fopen("test.txt","w");//打开文件
//text.txt在程序文件夹中可以直接这样写(相对路径),而绝对路径是完整的路径名
//w是只写,r是只读
//之间可以对文件进行操作
fclose(fp);//关闭文件
return 0;
}
文件的打开方式mode
文件的打开方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
r 只读 | 为了输入数据,打开一个已存在的文本文件 | 出错 |
rb 只读 | 为了输入数据,打开一个已存在的二进制文件 | 出错 |
w 只写 | 为了输出数据,打开一个文本文件 | 新建文件 |
wb 只写 | 为了输出数据,打开一个二进制文件 | 新建文件 |
a 追加 | 向文本文件尾部添加数据 | 出错 |
ab 追加 | 向二进制文件尾部添加数据 | 出错 |
r+ 读写 | 为了读和写,打开一个文本文件 | 出错 |
w+ 读写 | 为了读和写,打开一个文本文件 | 新建文件 |
a+ 读写 | 为了读和写,打开一个文本文件 | 出错 |
rb+ 读写 | 为了读和写,打开一个二进制文件 | 出错 |
wb+ 读写 | 为了读和写,打开一个二进制文件 | 新建文件 |
ab+ 读写 | 为了读和写,打开一个二进制文件 | 出错 |
四.文件的读写
1.顺序读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输入流 |
文件行输入函数 | fgets | 所有输入流 |
文件行输出函数 | fputs | 所有输入流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
fputc函数
C库函数int fputc(int char,FILE*stream)
把参数char指定的字符(一个无符号字符)写入到指定的流stream中,并把位置标识符往前移动。
参数
- char-这是要被写入的字符。该字符以其对应的int值进行传递
- stream-这是指向FILE对象的指针,即输出流
返回值
成功时,返回被写入字符;失败时,返回EOF并设置stream上的错误指示器(见ferror()
).
实例:
#include<stdio.h>
int main(){
FILE* fp;
int ch;
fp=fopen("file.txt","w+");
for(ch=33;ch<=100;ch++){//遍历文件中从第33到100的字符
fputc(ch,fp);//把ch指定的字符写入到文件流fp中
}
fclose(fp);
return 0;
}
fgetc函数
C库函数int fgetc(FILE* stream)
从指定的流stream获取下一个字符(一个无符号字符),并把位置标识往前移动。
参数
- stream-这是指向FILE对象的指针,即读取字符的来源
返回值
成功时以unsigned char
获得并强制转换为int的形式返回读取的字符,失败时为EOF。
若文件尾条件导致失败,则另外设置stream上的文件尾指示器(见feof()
)。若某些其他错误导致失败,则设置stream上的错误指示器(见ferror()
)。
实例:
#include<stdio.h>
int main(){
FILE* fp;
int c;
int n=0;
fp=fopen("file.txt","r");
if(fp==NULL){
perror("打开文件时发生错误");
return(-1);//返回一个代数值,表示该函数失败
}
do{
c=fgetc(fp);
if(feof(fp)){//检查是否已抵达给定文件流大的结尾
break;
}
printf("%c",c);
}while(1);//这是一个死循环,代码不再向下执行
fclose(fp);
return(0);
}
fputs函数
C库函数int fputs(const char* str,FILE* stream)
把字符串写入到指定的流stream中,但不包括空字符。
如同通过重复执行fputc。
参数
- str-这是一个数组,包含了要写入的空终止字符串
- stream-这是指向FILE对象的指针,该FILE对象标识了要被写入字符串的流,即输出流
返回值
成功时,返回非负值;失败时,返回EOF并设置stream上的错误指示器(见ferror
)。
实例:
#include<stdio.h>
int main(){
FILE* fp;
fp=fopen("file.txt","w+");
fputs("这是C语言。",fp);
fputs("这是一种系统程序设计语言。",fp);
fclose(fp);
return 0;
}
fgets函数
C库函数char* fgets(char* str,int n,FILE* stream)
从指定的流stream读取一行,并把它储存在str所指向的字符串内。当读取(n-1)个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
参数
- str-这是指向一个字符数组的指针,该数组存储了要读取的字符串。
- n-这是要读取的最大字符数(包括最后的空字符)。通常是使用以str传递的数组长度。
- stream-这是指向FILE对象的指针,即读取数据来源的文件流
返回值
成功时,该函数返回相同的str参数,失败时(到达文件末尾或者没有读取到任何字符或者发生错误)为空指针。
实例:
#include<stdio.h>
int main(){
FILE* fp;
char str[60];
fp=fopen("file.txt","r");//打开用于读取的文件
if(fp==NULL){
perror("打开文件时发生错误");
retur(-1);
}
if(fgets(str,60,fp)!=NULL){
puts(str);//向标准输出stdout写入内容
}
fclose(fp);
return(0);
}
fprintf函数
C库函数int fprintf(FILE* stream,const char* format,...)
发送格式化输出到流stream中。
参数
-
stream-要写入的输出文件流
-
format-这是C字符串,包含了要被写入到流stream中的文本。它可以包含嵌入的format标签,format标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format标签属性是
具体讲解如下:
flags(标识) 描述 - 在给定的字段宽度内左对齐,默认是右对齐(参见width子说明符) + 强制在结果之前显示加号或减号(+或-),即正数前面会显示+号。默认情况下,只有负数前面会显示一个-号 (space) 如果没有写入任何符号,则在该值前面插入一个空格 # 与o.x或X说明符一起使用时,非零值前面会分别显示0,0x或0X。与e,E和f一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示小数点。与g或G一起使用e或E时相同,但是尾部的零不会被移除。 0 在指定填充padding的数字左边按放置零(0),而不是空格(参见width子说明符) width(宽度) 描述 (number) 要输出的字符的最小数目。如果输出的值短于该数,结果会用空格填充。如果输出的值长于该数,结果不会被截断。 * 宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。 .precision(精度) 描述 .number 对于整数说明符(d,i,o,u,x,X):precision指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为0意味着不写入任何字符。对于额,E和f说明符:要在小数点后输出的小数位数。对于g和G说明符:要输出的最大有效位数。对于s:要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。 对于 c 类型:没有任何影响。 当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。 .* 精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。 length(长度) 描述 h 参考被解释为短整型或无符号短整型(仅适用于整数说明符:i,d,o,u,x和X) l 参数被解释为长整型或无符号长整形,适用于整数说明符(i,d,o,u,x和X)及说明符c(表示一个宽字符)和s(表示宽字符字符串) L 参数被解释为长双精度型(仅适用于浮点数说明符:e,E,f,g和G) specifier(说明符) 输出 c 字符 d或i 有符号十进制整数 e 使用e字符的科学计数法 E 使用E字符的科学计数法 f 十进制浮点数 g 自动选择%e或%f中合适的表示法 G 自动选择%E或%f中合适的表示法 o 有符号八进制 s 字符的字符串 u 无符号十进制整数 x 无符号十六进制整数 X 无符号十六进制整数(大写字母) p 指针地址 n 无输出 % 字符 -
附加参数-根据不同的format字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了format参数中指定的每个%标签。参数的个数应与%标签的个数相同。
返回值
成功时,返回写入的字符总数,失败时返回一个负数。
#include<stdio.h>
#include<stdlib.h>
int main(){
FILE* fp;
fp=fopen("file.txt","w+");
fprintf(fp,"%s %s %s %d","We","are","in",2024);//发送格式化输出到文件流fp中
fclose(fp);
return(0);
}
fscanf函数
C库函数int fscanf(FILE* stream,const char* format,...)
从流stream读取格式化输入,即按照format转译并将结果存储到指定位置。
参数
-
stream-这是指向FILE对象的指针
-
format-C字符串,包含了一下各项中的一个或多个:空格字符,非空格字符和format说明符。format说明符形式为
具体讲解如下: -
参数 描述 * 这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。 width 这指定了在当前读取操作中读取的最大字符数。 modifiers 为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g) type 一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见下一个表格。 fscanf类型说明符:
类型 合格的输入 参数的类型 c 单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。 char * d 十进制整数:数字前面的 + 或 - 号是可选的。 int * e,E,f,g,G 浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4 float * o 八进制整数 int * s 字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。 char * u 无符号的十进制整数 unsigned int * x,X 十六进制整数 int * -
附加参数-根据不同的format字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了format参数中指定的每个%标签。参数的个数应与%标签的个数相同。
返回值
成功时,返回成功匹配和赋值的个数,失败时(如果到达文件末尾或发生读错误),则返回EOF
实例:
#include<stdio.h>
#include<stdlib.h>
int main(){
char str1[10],str2[10],str3[10];
int year;
FILE* fp;
fp=fopen("file.txt","w+");
fputs("We are in 2024",fp);//把字符串写入到文件流fp中
rewind(fp);
fscanf(fp,"%s %s %s %d",str1,str2,str3,&year);//从文件流fp中读取格式化输入
printf("Read String1 |%s|\n",str1);
printf("Read String2 |%s|\n",str2);
printf("Read String3 |%s|\n",str3);
printf("Read Integer |%d|\n",year);
fclose(fp);
return(0);
}
fwrite函数
C库函数size_t fwrite(const void* ptr,size_t size,size_t nmemb,FILE* stream)
把ptr所指向的数组中的数据写入到给定流stream中。
参数
- ptr-这是指向要被写入的首个元素数组的指针
- size-这是要被写入的每个元素的大小,以字节为单位
- nmemb-这是元素的个数,每个元素的大小为size字节,即要被写入的对象数
- stream-这是指向FILE对象的指针,即指向输出流的指针
返回值
成功时,返回一个size_t对象,表示元素的总数
fread函数
C库函数size_t fread(void* ptr,size_t size,size_t nmemb,FILE* stream)
从给定流stream读取数据到ptr所指向的数组中。
参数
- ptr-这是指向带有最小尺寸
size* nmemb
字节的内存块的指针 - size-要读取的每个元素的大小,以字节为单位
- nmemb-元素的个数,每个元素的大小为size字节
- stream-指向FILE对象的指针,即读取来源的输入文件流
返回值
成功时,成功读取的元素总数会以size_t对象返回,size_t对象是一个整型数据类型。
如果总数与nmemb参数不同,则可能发生了一个错误或者到达了文件末尾。
#include<stdio.h>
#include<string.h>
int main(){
FILE* fp;
char c[]="This is runoob";
char buffer[20];
fp=fopen("file.txt","w+");//打开文件用于读写
fwrite(c,strlen(c)+1,1,fp);//写入数据到文件
fseek(fp,0,SEEK_SET);//查找文件的开头
fread(buffer,strlen(c)+1,1,fp);//读取并显示数据
printf("%s\n",buffer);
fclose(fp);
return(0);
}
2.随机读写
fseek函数
C库函数int fseek(FILE* stream,long int offset,int whence)
设置文件流stream
的文件位置为给定的偏移offset
所指向的值,参数offset意味着从给定的whence
位置查找的字节数。
即根据文件指针的位置和偏移量来定位文件指针。
FILE与FILE stream的区别
FILE是一个静态类,FILE stream是一个非静态类;
最直接区别:将读取文件比作是从A桶往B桶运水。使用FILE就是整个用桶倒进去,使用FILE stream就是使用水管慢慢输送,应用场景不同,大文件推荐FILE stream,不会炸内存。
FILE:是一个文件的类,对文件进行操作。其内部封装了对文件的各种操作(MSDN:提供用于创建,复制,删除,移动和打开单一文件的静态方法,并协助创建FILE stream对象)。
FILE stream:是一个文件流的类,处理文件的原始字节,即处理byte[]。对txt,xml,avi等任何文件进行内容写入,读取,复制…
参数
-
stream-这是指向FILE对象的指针,即要修改的文件流
-
offset-这是相对whence的偏移量,以字节为单位。
-
whence-这是表示开始添加偏移offset的位置。它一般指定为下列常量之一:
常量 描述 操作 SEEK_SET 文件的开头 即从文件的最前端处开始向后偏移offset字节 SEEK_CUR 文件指针的当前位置 即当前文件指针的偏移处开始向后偏移offset字节 SEEK_END 文件的末尾 即从文件的最末尾处开始向前偏移offset字节,当然在偏移数一定要为负数才能读取文件中的内容。
返回值
如果成功,则该函数返回为零,否则返回非零值。
实例:
#include<stdio.h>
int main(){
FILE* pFile;//定义一个文件
pFILE = fopen("example.txt","wb");//以读写形式打开这个二进制文件
fputs ("This is an apple.",pFile);//字符形式输出该文件
fseek (pFile,9,SEEK_SET);//从该文件开头偏移9字节的位置读取
fputs ("sam",pFile);//
fclose (pFile);//关闭文件
return 0;
}
ftell函数
C库函数```long int ftell(FILE* stream)返回给定流stream的当前位置,即返回文件指针相对于起始位置的偏移量。
参数
- stream-这是指向FILE对象的指针,即要检验的文件流
返回值
成功时,返回位置标识符的当前值,失败时,返回-1L,全局变量errno被设置为一个正值。
实例:
#include<stdio.h>
int main(){
FILE* fp;
int len;
fp=fopen("file.txt","r");
if(fp==NULL){
perror("打开文件错误");
return(-1);
}
}
rewind函数
C库函数void rewind(FILE* stream)
设置文件位置为给定流stream的文件的开头。
参数
- stream-要修改的文件流
返回值
无
实例:
#include<stdio.h>
int main(){
int n;
FILE* pFile;
char buffer[27];
pFile=fopen("myfile.txt","w+");
for(n='A';n<='Z';n++)
fputc(n,pFile);
rewind(pFile);
fread(buffer,1,26,pFile);
fclose(pFile);
buffer[26]='\0';
puts(buffer);
return 0;
}
五.文件读取结果的判定
feof函数的错误使用:
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。(feof函数是判断结束过程而不是判断结束的结果)
文本文件读取是否结束,判断返回值是否为EOF(getc)或者NULL(fgets)
例如:
- fgetc判断是否为EOF
- fgets判断返回值是否为NULL
实例:
#include<stdio.h>
#include<stdlib.h>
int main(void){
int c;//注意:int,非char,要求处理EOF
FILE* fp=fopen("test.txt","r");
if(!fp){
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while((c=fgetc(fp))!=EOF){//标准C I/O读取文件循环
putchar(c);
}
if(ferror(fp))//判断是什么原因结束的
puts("I/O error when reading");
else if(feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
二进制文件读取是否结束,判断返回值是否小于实际要读的个数。
例如:fread判断返回值是否小于实际要读的个数
实例:
#include<stdio.h>
enum{SIZE=5};
int main(void){
double a[SIZE]={1.,2.,3.,4.,5.};
FILE* fp=fopen("test.bin","wb");//必须用二进制形式
fwrite(a,sizeof* a,SIZE,fp);//写double的数组
}
六.文件缓冲区
说到文件缓冲区,我们就自然而然想到输入缓冲区,即当一个字符一个字符从键盘上输入时,并不是直接输入到磁盘内,而是先放到输入缓冲区,而当输入缓冲区内的字符放满后,文件缓冲区才向磁盘内输入字符。
文件缓冲区也是一样的道理。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
测试代码:
#include<stdio.h>
#include<windows.h>
//VS2013 WIN10环境测试
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;
}
我们可以测试一下这个代码,再程序第一个到fgets函数处时,立刻去打开test.txt文本文件,我们会发现里面没有内容,而我们用刷新文件缓冲区的fflush函数再次打开test.txt文本文件时,会发现里面已经有输入的内容,则能够证实的确有文件缓冲区的存在。
因为有文件缓冲区的存在,C语言再操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。