目录:
(1)为什么使用文件
(2)什么是文件
(3)二进制文件和文本文件
(4)文件的打开和关闭
(5)文件的顺序读写
(6)文件的随机读写
(7)文件读取结束的判定
(8)文件缓冲区
1. 为什么要使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
2. 什么是文件?
磁盘(硬盘)上的文件是文件。 但是在程序设计中,我们⼀般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows 环境后缀为.exe)。
2.2 数据文件
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。 其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3 文件名
⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
3. 二进制文件和文本文件?
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式储存,如果不加转换的输出到外存的文件中,就是二进制文件。
如果要求在外存上以ASCII码的形式储存,则需要在储存前转换。以ASCII码字符的形式储存的文件就是文本文件。
一个数据在文件中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式储存,也可以使用而二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
#include<stdio.h> int main() { int a = 10000; FILE* pf = fopen("text.txt","wb"); fwrite(&a,4,1,pf); fclose(pf); pf = NULL; return 0; }
4. 文件的打开和关闭
4.1.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘的等的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
4.1.2 标准流
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
(1)stdin —— 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
(2)stdout —— 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
(3)stderr —— 标准错误流,大多数环境中输出到显示器界面
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入和输出操作的。stdin、stdout、stderr三个流的类型是:FILE* ,通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
4.2 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件地都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字、文件状态以及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE
例如,VS2013编译环境提供的stdio.h头文件中有以下的文件类型申明:
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结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过发i文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与他关联的文件。
4.3 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE* 的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC规定使用fopen函数来打开文件,fclose函数来关闭文件。
//fopen函数原型 FILE* fopen(const char* filenamen,const char* mode); //文件名,文件的打开方式 //fclose函数原型 int fclose(FILE* stream); //流
mode表示文件的打开模式,下面都是文件的打开模式:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件(文件若存在,清空原内容) | 建立一个新的文件 |
"a"(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
"rb+"(读写) | 为了读和写打开一个二进制文件 | 出错 |
"wb+"(读写) | 为了读和写打开一个二进制文件 | 建立一个新的文件 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
/* fopen fclose example */ #include <stdio.h> int main () { FILE * pFile; //打开⽂件 pFile = fopen ("myfile.txt","w"); //绝对路径:文件路径+文件名主干+文件后缀 //相对路径: //. 表示当前路径 //.. 表示上一级路径 // .\\..\\test.txt —— 打开当前代码所在的路径上一级的test.txt //⽂件操作 if (pFile!=NULL) { fputs ("fopen example",pFile); //关闭⽂件 fclose (pFile); pFile = NULL; } return 0; }
5. 文件的顺序读写
5.1 顺序读写函数介绍
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
上述函数除了最后两个,其他都是读写的都是文本信息;
上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)
(1)fgetc和fputc使用
#include<stdio.h> int main() { FILE* pf = fopen("text.txt","w+"); if(pf == NULL) { perror("fopen"); return 1; } //写文件 //1.fputc函数原型:int fputc ( int character, FILE * stream ); // fputc('a',pf); // fputc('b',pf); // fputc('c',pf); // fputc('d',pf); // fputc('e',pf); // fputc('f',pf); // fputc('g',pf); // fputc('h',pf); // fputc('i',pf); //2. fgetc函数原型:int fgetc ( FILE * stream ); // 返回值 // 成功后,将返回字符 read(提升为 int 值)。返回ASCII码值 // 返回类型为 int 以容纳特殊值 EOF,该值指示失败; // 如果位置指示符位于文件末尾,则函数返回 EOF 并设置 stream 的 eof 指示符 (feof)。 // 如果发生其他读取错误,该函数还会返回 EOF,但会设置其错误指示符(ferror)。 int ch = 0; while((ch = fgetc(pf)) != EOF) { printf("%c ",ch); } //关闭文件 fclose(pf); pf = NULL; return 0; }
int main() { int ch = fgetc(stdin); fputc(ch,stdout); return 0; }
(2)fputs和fgets的使用
int main() { //打开文件 FILE* pf = fopen("text.txt","w+"); if(pf == NULL) { perror("fopen"); return 1; } //写文件 //1. fput函数原型:int fputs ( const char * str, FILE * stream ); fputs("aaaaaaaaaaa!",pf); fputs("are you ok",pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
fgets返回值
成功后,该函数返回 str。 如果在尝试读取字符时遇到文件结尾,则设置 eof 指示符 (feof)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。 如果发生读取错误,则设置错误指示符 (ferror) 并返回 null 指针(但 str 指向的内容可能已更改)。
int main() { //打开文件 FILE* pf = fopen("text.txt","r"); if(pf == NULL) { perror("fopen"); return 1; } //读文件 //2. fgets函数原型:char * fgets ( char * str, int num, FILE * stream ); //读文件 char arr[20] = "xxxxxxx"; fgets(arr,3,pf);//实际上只从文件读取四个+最后添一个\0//遇到换行也结束 printf("%s\n",arr); //关闭文件 fclose(pf); pf = NULL; return 0; }
(3)fscanf和fprintf的运用
fprintf函数:
int main() { //打开文件 FILE* pf = fopen("text.txt","w"); if(pf == NULL) { perror("fopen"); return 1; } //写文件 //1. fprintf函数原型:int fprintf ( FILE * stream, const char * format, ... );//... —— 可变参数列表 //2. printf函数原型:int printf ( const char * format, ... ); char name[10] = "lisi"; int age = 20; float score = 90.0f; fprintf(pf,"%s %d %.1f",name,age,score); //关闭文件 fclose(pf); pf = NULL; return 0; }
fscanf函数:
int main() { //打开文件 FILE* pf = fopen("text.txt","r"); if(pf == NULL) { perror("fopen"); return 1; } //读文件 //1. fscanf函数原型:int fscanf ( FILE * stream, const char * format, ... ); char name[20]; int age; float score; fscanf(pf,"%s %d %f",name,&age,&score); //打印在屏幕上 printf("%s %d %f\n",name,age,score); fprintf(stdout,"%s %d %f\n",name,age,score); //关闭文件 fclose(pf); pf = NULL; return 0; }
fread和fwrite函数使用
struct S { char name[20]; int age; float score; }; int main() { struct S s = {"cuihua",25,88.8f}; //以二进制形式写到文件中 //打开文件 FILE* pf = fopen("text.txt","wb"); if(pf == NULL) { perror("fopen"); } //fwrite函数原型:size_t fwrite(const void*ptr,size_t size,size_t count,FILE *stream); // 内存块 每个元素大小 元素数 流 //将ptr中count个,大小为size个字节的数据,写到文件中 //将数据块写入流 //将 count 元素数组(每个元素的大小为 size 字节)从 ptr 指向的内存块写入流中的当前位置。 //流的位置指示器按写入的总字节数前进。 //在内部,该函数将指向的块解释为 ,就好像它是 type 为 的元素数组 ,并按顺序将它们写入,就像为每个字节调用一样。 //写文件 fwrite(&s,sizeof(struct S),1,pf); //关闭文件 fclose(pf); pf = NULL; }
struct S { char name[20]; int age; float score; }; int main() { struct S s = {0}; //读取二进制的信息到文件中 //打开文件 FILE* pf = fopen("text.txt","rb"); if(pf == NULL) { perror("fopen"); return 1; } //fread函数原型:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); // 内存块 每个元素大小 元素数 流 //从文件中读取count个大小为size个字节的数据,存放到ptr指向的空间 //从流中读取数据块 //从流中读取 count 元素数组,每个元素的大小为 size 字节,并将它们存储在 ptr 指定的内存块中。 //流的位置指示器按读取的总字节数前进。 //如果成功,则读取的总字节数为 (size*count)。 //读文件 fread(&s,sizeof(struct S),1,pf); printf("%s %d %f\n",s.name,s.age,s.score); //关闭文件 fclose(pf); pf = NULL; }
5.2 对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
(1)scanf / printf —— 针对标准输入流 / 标准输出流的 格式化 输入/输出函数
(2)fscanf / fprintf —— 针对所有输入流 / 所有输出流的 格式化 输入/输出函数
(3)sscanf / sprintf
sprintf
int sprintf ( char * str, const char * format, ... );
将格式化数据写入字符串(将格式化数据转成字符串)
使用在 printf 上使用 format 时打印的相同文本编写字符串,但内容不是打印,而是作为 C 字符串存储在 str 指向的缓冲区中。
缓冲区的大小应该足够大,以包含整个结果字符串(有关更安全的版本,请参阅 snprintf)。
终止 null 字符会自动附加到内容之后。
在 format 参数之后,该函数至少需要与 format 所需的相同数量的附加参数。
struct S { char name[20]; int age; float score; }; int main() { char arr[100] = {0}; struct S s = {"wangwu",23,66.6}; sprintf(arr,"%s %d %f",s.name,s.age,s.score); printf("%s\n",arr); return 0; }
sscanf
int sscanf ( const char * s, const char * format, ...);
从字符串中读取格式化数据(将字符串转成格式化数据)
从 s 读取数据,并根据参数格式将它们存储到附加参数给出的位置,就像使用 scanf 一样,但从 s 而不是标准输入 (stdin) 读取。
其他参数应指向已分配的对象,该对象由格式字符串中的相应格式说明符指定的类型。
struct S { char name[20]; int age; float score; }; int main() { char arr[100] = {0}; struct S s = {"wangwu",23,66.6}; //创建临时变量 struct S tmp= {0}; //将s中的各个数据转换成字符串,存放在arr中 sprintf(arr,"%s %d %f",s.name,s.age,s.score); //从字符串arr中提取格式化的数据,存放在tmp中 sscanf(arr,"%s %d %f",tmp.name,&(tmp.age),&(tmp.score)); printf("%s %d %f\n",tmp.name,tmp.age,tmp.score); return 0; }
6. 文件的随机读写
6.1 fseek
根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)
int fseek ( FILE * stream, long int offset, int origin );
重新定位流位置指示器
将与流关联的位置指示器设置为新位置。
对于以二进制模式打开的流,新位置是通过向 origin 指定的参考位置添加 offset 来定义的。
对于以文本模式打开的流,offset 应为零或上一次调用 ftell 返回的值,并且 origin 必须为 SEEK_SET
。
如果使用这些参数的其他值调用函数,则支持取决于特定的系统和库实现(不可移植)。
成功调用此函数后,将清除流的文件结束内部指示器,并且之前对此流的 ungetc 调用的所有效果都将被删除。
在打开进行更新(读 + 写)的流上,对 fseek
的调用允许在读取和写入之间切换。
参数:
FILE * stream:指向标识流的 FILE 对象的指针。
long int offset:
Binary files:要从源偏移的字节数;文本文件:零或 ftell 返回的值。(左偏负右偏正)
int origin:起始位置
用作偏移参考的位置。它由 中定义的以下常量之一指定,专门用于用作此函数的参数:
constant | 参考位置 |
---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件结束 * |
* 允许库实现不有意义地支持 SEEK_END
(因此,使用它的代码没有真正的标准可移植性)。
例子:
#include <stdio.h> int main () { FILE * pFile; pFile = fopen ( "example.txt" , "wb" ); fputs ( "This is an apple." , pFile ); fseek ( pFile , 9 , SEEK_SET ); fputs ( " sam" , pFile ); fclose ( pFile ); return 0; }
6.2 ftell
返回当下文件指针(光标)相对于文件的起始位置的偏移量
long int ftell ( FILE * stream );
获取流中的当前位置
返回流的位置指示器的当前值。
对于二进制流,这是从文件开头开始的字节数。
对于文本流,数值可能没有意义,但仍可用于稍后使用 fseek 将位置恢复到相同位置(如果有使用 ungetc 放回的字符仍在等待读取,则行为未定义)。
返回值
成功后,将返回位置指示器的当前值。 失败时,返回 -1L
,并将 errno 设置为特定于系统的正值。
例子:
#include <stdio.h> int main () { FILE * pFile; long size; pFile = fopen ("myfile.txt","rb"); if (pFile == NULL) perror ("Error opening file"); else { fseek (pFile, 0, SEEK_END);//光标移动到结尾 size = ftell(pFile);//当前光标相对文件起始位置光标的偏移量 fclose (pFile); printf ("Size of myfile.txt: %ld bytes.\n",size); } return 0; }
6.3 rewind
让文件指针(光标)位置回到文件的起始位置
void rewind ( FILE * stream );
将流的位置设置为开头
将与 stream 关联的位置指示器设置为文件的开头。
成功调用此函数后,将清除与流关联的文件结束和错误内部指示符,并删除之前对此流的 ungetc 调用的所有影响。
在打开进行更新(读 + 写)的流上,对 rewind
的调用允许在读取和写入之间切换。
#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'; printf(buffer); return 0; }
7. 文件读取结束的判定
7.1 被错误使用的feof(经常被用错)
feof函数原型:
int feof ( FILE * stream );
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件是否结束。
feof的作用不是根据返回值来判断文件是否结束;
feof的作用而是:当文件读取结束的时候,判断读取结束的原因是否是:遇到文件尾结束。
但是文件操作结束的原因可能有两个情况:
(1)遇到文件末尾
(2)遇到错误了
feof无法处理遇到错误的情况。
1.文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
例如:
fgetc判断是否为EOF;
fgets判断返回值是否为NULL;
2.二进制文件的读取结束判断,判断返回值是否小于世纪要读的个数。
例如:
fread判断返回值是否小于实际要读的个数
文本文件的例子:
#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))//ferror函数会检测是否是I/O发生错误而导致的结束 puts("I/O error when reading"); else if (feof(fp)) puts("End of file reached successfully"); fclose(fp); }
二进制文件的例子:
#include <stdio.h> enum { SIZE = 5 };//枚举类型 int main(void) { double a[SIZE] = {1.,2.,3.,4.,5.};//1.就是1.0 FILE *fp = fopen("test.bin", "wb"); //必须⽤⼆进制模式 fwrite(a, sizeof *a, SIZE, fp); //写double 的数组 fclose(fp); double b[SIZE]; fp = fopen("test.bin","rb"); size_t ret_code = fread(b, sizeof *b, SIZE, fp); // double 的数组 if(ret_code == SIZE) { puts("Array read successfully, contents: "); for(int n = 0; n < SIZE; ++n) { printf("%f ", b[n]); } putchar('\n'); } else { // error handling if (feof(fp)) printf("Error reading test.bin: unexpected end of file\n"); else if (ferror(fp)) //检查是否设置了与 stream 关联的错误指示符,如果设置了,则返回一个不为零的值。 { perror("Error reading test.bin"); } } fclose(fp); }
写一个实战代码:拷贝文件
将test1.txt的内容拷贝到test2.txt
int main() { //打开文件 FILE* pfread = fopen("test1.txt","r"); if(pfread == NULL) { perror("fopen"); return 1; } FILE* pfwrite = fopen("test2.txt","w"); if(pfwrite == NULL) { perror("fopen"); fclose(pfread); pfread = NULL; return 1; } //读写文件 int ch = 0; while((ch = fgetc(pfread)) != EOF) { fputc(ch,pfwrite); } //关闭文件 fclose(pfread); pfread = NULL; fclose(pfwrite); pfwrite = NULL; return 0; }
8. 文件缓冲区的概念
ANSIC标准采用”缓冲文件系统“处理数据文件的,所谓缓冲文件系统就是至系统自动地在内存中为程序中每一个正在使用的文件开辟一块”文件缓冲区“。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满内存缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定。
#include <stdio.h> #include <windows.h> //VS2022 WIN11环境测试 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; }
祝大家元旦快乐!!!新的一年继续好好学习、认真工作。难搞的人生需要乐在其中,2025继续加油!