目录
(二)、返回文件指针相对于起始位置的偏移量——ftell函数
一、为什么使用文件
在我们前面练习使用结构体时,写通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。 使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
二、什么是文件(文件的概念和分类)
磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
(一)、程序文件
包括源程序文件(后缀为 .c ) , 目标文件( windows 环境后缀为 .obj ) , 可执行程序( windows 环境后缀为.exe )。
(二)、数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
注意: 接下来我们 讨论的是数据文件。在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
三、文件名
①:一个文件要有一个 唯一的 文件标识,以便用户识别和引用。②:文件名包含3 部分:文件路径 + 文件名主干 + 文件后缀例如: c:\code\test.txt③:为了方便起见,文件标识常被称为 文件名 。
四、文件的打开和关闭
(一)、文件指针
缓冲文件系统中,关键的概念是 “ 文件类型指针 ” ,简称 “ 文件指针 ” 。每个被使用的文件都在内存中开辟了一个相应的 文件信息区 ,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统 声明的, 取名FILE.
例如, VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型申明:struct _iobuf { char* _ptr; int _cnt; char* _base; int _flag; int _file; int _charbuf; int _bufsiz; char* _tmpfname; }; typedef struct _iobof FILE;
不同的 C 编译器的 FILE 类型包含的内容不完全相同,但是大同小异。每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE 结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个FILE 的指针来维护这个 FILE 结构的变量,这样使用起来更加方便。
(二)、文件的打开和关闭
文件在读写之前应该先 打开文件 ,在使用结束之后应该 关闭文件 。在编写程序的时候,在打开文件的同时,都会返回一个 FILE*的指针 变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用 fopen函数 来打开文件, fclose 来关闭文件。//打开文件 FILE* fopen(const char* filename, const char* mode); //关闭文件 int fclose(FILE* stream);
1.打开文件——fopen函数
1.1函数声明:
FILE * fopen ( const char * filename, const char * mode );
①:第一个参数是待打开文件的文件名;
而上面提到。文件名包含三个部分:文件路径+文件名主干+文件后缀
而文件路径又有两种:
第一种是相对路径,如下面一个使用实例中“data.txt”就是相对路径,就是在此路径下寻找,只区分文件名主干+后缀。
第二种是绝对路径,就是指一个完整的文件路径。
②:第二个参数是打开文件的方式,如下图:
③:函数返回类型:
如果文件成功打开,该函数将返回指向 FILE 对象的指针,该对象可用于在将来的操作中标识流;
如果打开失败,则将返回空指针。
1.2函数使用实例
我们以“r”的方式打开文件,若当前路径文件存在,则正常打开;文件不存在则打开失败。
int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } printf("打开成功!\n"); return 0; }
可以看见,当前路径下是没有"data.txt"这个文件的,所以打开出错,fopen返回NULL,运行结果如下:
当我提前创建了“data.txt”文件后,则打开成功。如下图:
所以我们要根据需要,合理选择打开方式。
2.关闭文件——fclose函数
关闭文件很简单,和free类似,如下:
int main() { //打开文件 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } printf("打开成功!\n"); //关闭文件 fclose(pf); pf = NULL; return 0; }
五、文件的顺序读写
首先需要先搞清楚,什么是写文件(输出),什么是读文件(输入)?
将数据放在文件里面,就叫写文件;
从文件里面拿数据出来,就叫读文件。
(一)、文件的顺序读写函数的介绍:
下面演示几个函数:
1.fputc函数:写文件
①:函数声明:
int fputc ( int character, FILE * stream );
参数一是要编写的字符的 int 提升。写入时,该值在内部转换为无符号字符。
参数二是指向标识输出流的 FILE 对象的指针。
返回值:
操作成功后,将返回所写字符。
如果发生写入错误,则返回 EOF 并设置错误指示器(ferror)。②:使用实例:
int main() { //打开文件 FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } printf("打开成功!\n"); //写文件 fputc('a', pf); fputc('b', pf); fputc('c', pf); fputc('1', pf); fputc('2', pf); fputc('3', pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
2.fgetc函数:读文件
①:函数声明:
int fgetc ( FILE * stream );
参数是指向标识输入流的 FILE 对象的指针。
返回值:
返回指定流的内部文件位置指示符当前指向的字符。然后,内部文件位置指示器将前进到下一个字符。
如果调用时流位于文件末尾,则该函数返回 EOF 并为流设置 (feof) 的文件结束指示器。
如果发生读取错误,该函数将返回 EOF 并为流设置错误指示器 (ferror)。
FGETC 和 GETC 是等效的,只是 GETC 可能在某些库中作为宏实现。②:函数使用实例:
int main() { //打开文件 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror(fopen); return 1; } //读文件 //一次读取一个字符 printf("%c ", fgetc(pf)); printf("%c ", fgetc(pf)); printf("%c \n", fgetc(pf)); //读取键盘的字符(键盘输入) printf("%c ", fgetc(stdin)); printf("%c ", fgetc(stdin)); printf("%c ", fgetc(stdin)); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
3.fputs函数:写文件(一行)
与fputc不同的是,fputc一次只能写一个字符,而fputs函数一次可以写一行;
使用实例:
int main() { //打开文件 FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } printf("打开成功!\n"); //写文件 fputs("abcdef", pf); fputs("12345", pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果
但这里需要注意fputs不会自动换行和空格,如上;
需要换行我们可以在末尾加上‘\n’;
标准输出流
4.fgets函数:读文件(一行)
类似的,fgetc函数是一次读一个字符;而fgets函数是一次读一行数据;但有一些不同的地方,如下:
函数声明:
char * fgets ( char * str, int num, FILE * stream );
①:读文件规则:
从流中读取字符并将其作为 C 字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件末尾,以先发生者为准。
换行符使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
终止空字符会自动附加到复制到 str 的字符之后。
请注意,fgets 与 get 完全不同:fgets 不仅接受流参数,还允许指定 str 的最大大小,并在字符串中包含任何结束换行符。②:第一个参数:指向在其中复制字符串读取的字符数组的指针。
③:第二个参数:要复制到 str 的最大字符数(包括终止空字符)。
④:第三个参数:
指向标识输入流的 FILE 对象的指针。或者
stdin 可以用作从标准输入读取的参数。⑤:返回值:
操作成功后,函数返回 str。
如果在尝试读取字符时遇到文件末尾,则设置 eof 指示器 (feof)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(str 的内容保持不变)。
如果发生读取错误,则设置错误指示器(ferror),并返回空指针(但str指向的内容可能已更改)。⑥:使用实例:
int main() { //打开文件 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 //一次读四个字符 char str[6] = { 0 }; fgets(str, 5, pf); printf("%s\n", str); fgets(str, 5, pf); printf("%s\n", str); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
可以看到第二次读取19个字符时,第一行已经读完了,该换行后fgets就读取结束,并不会继续读取第二行的数据;
我们介绍了表格中的前四个函数,并且前四个函数都是针对字符串的
而fprintf,fscanf是针对各种类型的(int char 结构体等等)
下面讲解第五和第六个函数——fprintf、fscanf
5.fprintf函数(写文件):使用实例
int main() { struct S s = { 10,'a',3.14 }; //打开文件 FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fprintf(pf, "%d %c %f", s.a, s.b, s.c); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
可以看到fprintf函数的使用方法和printf差不多,区别只在于fprintf函数多了一个参数,即流对象;
6.fscanf(读文件):使用实例
struct S { int a; char b; double c; }; int main() { struct S s = { 10,'a',3.14 }; //打开文件 FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } printf("打开成功!\n"); //写文件 //将文件的数据放入结构体成员中 fscanf(pf, "%d %c %lf", &(s.a), &(s.b), &(s.c)); //打印结构体成员 printf("%d %c %lf", s.a, s.b, s.c); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
类似的可以看到,fscanf和scanf类似,只是fscanf多了一个参数,即流对象;
7.sprintf/sscanf函数使用实例
注意:这张图需记住!
六、文件的随机读写
(一)、定位文件指针函数——fseek函数
要想随机读写文件,我们就要让文件指针指向指定位置,下面一个函数就可以达到目的
①:函数声明:
int fseek ( FILE * stream, long int offset, int origin );
②作用:
将与流关联的位置指示器设置为新位置。
对于以二进制模式打开的流,新位置是通过向源指定的参考位置添加偏移量来定义的。
对于以文本模式打开的流,偏移量应为零或先前调用 ftel 返回的值,并且源必须SEEK_SET。
如果使用这些参数的其他值调用函数,则支持取决于特定的系统和库实现(不可移植)。成功调用此函数后,流的文件结束内部指示器将被清除,并且之前对此流的 ungetc 调用的所有效果都将被删除。
在开放进行更新(读+写)的流上,对 fseek 的调用允许在读取和写入之间切换。③:函数参数:
参数一:指向标识流的 FILE 对象的指针。(待定位的文件流);
参数二:偏移量:正数(从左向右偏移);负数(从右向左偏移);
二进制文件:要从源偏移的字节数。
文本文件:零或 ftell 返回的值。参数三:起始位置;(从哪里开始偏移),一般情况有三种,如下:
④:函数返回值:
如果成功,该函数将返回零。
否则,它将返回非零值。
如果发生读取或写入错误,则设置错误指示器(ferror)。⑤:函数使用实例:
#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; }
(二)、返回文件指针相对于起始位置的偏移量——ftell函数
①:函数声明:
long int ftell ( FILE * stream );
这个函数使用很简单,如下:
函数使用实例:
(三)、让文件指针的位置回到起始位置——rewind函数
①:函数声明:
void rewind ( FILE * stream );
函数使用实例:
七、文本文件和二进制位文件
根据数据的组织形式,数据文件被称为 文本文件 或者 二进制文件 。数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是 二进制文件 。如果要求在外存上以 ASCII 码的形式存储,则需要在存储前转换。以 ASCII 字符的形式存储的文件就是 文 本文件。
八、文件读取结束标志的判定
(一)、被错误使用feof
牢记:在文件读取过程中,不能用 feof 函数的返回值直接来判断文件的是否结束。feof 的作用是:当文件读取结束的时候, 判断是读取结束的原因是否是:遇到文件尾结束。1. 在判断 文本文件读取是否结束时,就判断函数fgets返回值是否为 EOF ,或者 NULL例如: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.}; 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)) { perror("Error reading test.bin"); } } fclose(fp); }
实现文件的拷贝。
九、文件缓冲区
ANSIC 标准采用 “ 缓冲文件系统 ” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“ 文件缓冲区 ” 。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据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; }
这里可以得出一个 结论 :因为有缓冲区的存在, C 语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。