目录
9.比较scanf/fscanf/sscanf, printf/fprintf/sprintf
一、为什么要使用文件:(做到数据持久化保存)
我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
二、文件名解析:
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:
文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
三、编译器内部FILE定义:
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名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;每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:FILE* pf;定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
简而言之:我们想要使用文件,就得通过FILE指针来进行内部访问。
四、文件的打开和关闭(fopen,fclose)
文件在使用前应该先打开文件,使用完毕后应该关闭文件。(同时需要一个FILE类型的指针变量来进行存储)
我们可以看到fopen返回值类型为FILE指针,所以使用时应该用FILE的指针变量进行接收,第一个参数为char*类型,存放文件创建的位置,必须为字符类型,第二个参数为char*类型,存放的是打开文件的方式,必须从下面的打开方式中选择。
关闭文件方法和动态内存空间创造释放内存的free相似,不过多赘述。
//打开文件 FILE * fopen ( const char * filename, const char * mode ); //关闭文件 int fclose ( FILE * stream );例如:
//打开文件 FILE* pf=fopen("text.txt","w");//通过写的方式打开文件text.txt. if(pf==NULL) { perror("fopen");//判断pf是否为空指针.空指针则报fopen打开错误. } //关闭文件 fclose(pf); pf=NULL;打开方式如下:
五、文件的顺序读写:
注意:在进行输入操作时使用r,输出操作时使用w,二进制时使用wb和rb
间述文件读写和输入输出的关系:(主要是面向对象不同)
1、字符输出函数:fputc
值得注意这个函数,返回值为int类型,如果写入成功则会返回写入的数据,如果失败则会返回EOF,第一个参数为int character,表示是你要写入的数据,可以是数字、字符等,第二个参数为FILE类型的指针,表示你要给你写入的数据一个存放的位置,此处应该使用上面打开文件中创造的指针变量。
写入文件(逐个字符写入)(通过for循环依次写入26个字母到text.txt中)
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("text.txt", "w"); if(pf==NULL) { perror("fopen"); return 1; } //写入文件 /* fputc('a', pf); fputc('b', pf); fputc('c', pf);*/ int i = 0; for (int i = 0; i < 26; i++) { fputc('a' + i, pf); } //关闭文件 fclose(pf); pf=NULL; return 0; }创建了text.txt文件,向文件中写入26个英文字母。
2、字符输入函数:fgetc
返回值为int,用int类型整数接收,一个参数为FILE类型指针,应该使用上面文件打开时使用的指针变量。使用成功则会返回读取到的数据,使用失败则会返回EOF。
读文件(逐个字符读取)(读取上面文件中输入的26个英文字母输出到屏幕)
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("text.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读入文件 // 读取一个字符 //int a=fgetc( pf); //printf("%c\n", a); //通过for循环读取26个字符 //int i = 0; //for (int i = 0; i < 26; i++) //{ // int a=fgetc(pf); // printf("%c ", a); //} //通过fgetc的返回值读取字符,直到读到没有字符时停止 int ch = 0; while ((ch = fgetc(pf)) != EOF) { printf("%c ", ch); } //关闭文件 fclose; return 0; }输入如下:
这样我们就实现了向文件中写入一个字符,读出一个字符的方法啦,但是这还是相当的繁琐和多靠人为计算字符个数,不太智能。
所以介绍下一个方法:
3、文本行输出函数:fputs
返回值为int类型,用整形参数接收,如果成功返回输出的数据,如果失败返回EOF;第一个参数为char类型的指针变量,应该在此处输入你要写入的字符串(一行),他会写入你此处全部数据,第二个参数为FILE类型指针,使用上面打开文件时的指针接收。
向文件写入一行数据
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("text.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写入文件 fputs("hello\n", pf); fputs("world nice", pf); //关闭文件 fclose(pf); return 0; }如图:创建文件text.txt,写入数据hello和world nice,值得注意的是,我在fputs中写hello时输入了\n,他才能跳过这一行写到下一行,这也是这个函数的特点,要不然他会把文件第一行填满才会写入下一行,这是个值得注意的点~
4、文本行输入函数:fgets
返回类型为char*,应该使用char*指针类型或者数组类型接收参数,使用成功返回获取到的数据,使用失败则会返回空指针;第一个参数为char*类型,应该使用数组或者char*接收,这里一般写你要存放的位置,比如存放在a数组里;第二个参数为int类型,表示你要读取多少个数,如果过大则只会读完那一行的数就返回;第三个参数为FILE*类型,用上面打开文件时的指针接收。
读取文件中的两行数据:
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("text.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读入文件 char arr[20] = { 0 }; fgets(arr, 10, pf); printf("%s", arr); fgets(arr, 15, pf); printf("%s", arr); //关闭文件 fclose(pf); return 0; }可以看到和什么puts输入的数据一样这就是这两个函数的妙用。
5、格式化输出函数:fprintf
返回类型为整形,用int接收,如果成功返回输入的数,如果发生写入错误,则设置错误指示器(ferror)并返回负数。如果在写入宽字符时发生多字节字符编码错误,则 errno 将设置为 EILSEQ 并返回负数;第一个参数为FILE*类型,应该使用上面打开文件时使用的变量接收,第二个类型为char*类型,表示你要写入的数据,写入格式和printf的格式一样,“%d %d %d”,a,b,c;这样按格式输出。
例如:输出一个结构体的数据到文件中:
#include<stdio.h> struct S { char name[20]; int age; float score; }; int main() { struct S s = { "zhulimeng",20,10.1f }; //打开文件 FILE* pf = fopen("text.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fprintf(pf,"%s %d %.1f", s.name, s.age, s.score); //关闭文件 fclose(pf); return 0; }这样我们就可以在文件中看到输入的结构体的数据了。
6、格式化输入函数:fscanf
返回值为整形,用int接收,如果成功返回输入的数据,如果在读取时发生读取错误或到达文件末尾,则会设置正确的指示器(feof 或 ferror)。而且,如果在成功读取任何数据之前发生任何一种情况,则返回 EOF。第一个参数为FILE*类型,用上面打开文件时创立的指针接收,第二个参数为char*类型,此处需要按格式接收。
例如:创建一个相同成员的空结构体接收fprintf输入的值
#include<stdio.h> struct S { char name[20]; int age; float score; }; int main() { struct S s = { 0 }; //打开文件 FILE* pf = fopen("text.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 fscanf(pf,"%s %d %f", s.name, &(s.age), &(s.score)); printf("%s %d %.1f", s.name, s.age, s.score); //关闭文件 fclose(pf); return 0; }
我们就可以读取到文件中结构体的内容赋予到这个空结构体上了。
7、二进制输出函数:fwrite
返回值为size_t类型,返回成功的是你的操作数,操作了几次返回几,返回失败则会阻止程序的写入;第一个参数为void*类型,可以写入任何类型的数,一般是写写入的数据的变量,可以是int,char或者结构体类型;第二个参数为字节数,就是求第一个变量的字节数,可用sizeof来读取;第三个变量为写入数据的个数,一组数据就写1,依次类推;第四个类型是FILE*类型,通常写上面打开文件时的变量。
例:二进制写入结构体数据:(注意是wb读文件)
#include<stdio.h> struct S { char name[20]; int age; float score; }; int main() { struct S s = { "朱立檬",20,10.1f }; //打开文件 FILE* pf = fopen("text.txt", "wb");//二进制此处是wb if (pf == NULL) { perror("fopen"); return 1; } //写文件 fwrite(&s, sizeof(s), 1, pf); //关闭文件 fclose(pf); return 0; }由上图可以看到,在打开的文件中,写入的是我们看不懂的奇怪字符,这便是计算机的二进制文本表示,这样有利于写入数据的安全,后要取出数据可以通过下面这个函数实现。
8、二进制输入函数:fread
返回值为整形,用int接收,如果成功返回元素个数, 如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都设置了正确的指示器,可以分别使用铁道和feof进行检查。如果大小或计数为零,则该函数返回零,并且 ptr 所指向的流状态和内容保持不变。第一个为void*类型指针,可以存放任何类型数据,这里一般存放你要把读取到的数存放的变量,一一对应存放;第二个为字节数,也与第一个空的类型相同,用sizeof读取;第三个为需要读取的数据个数,根据实际情况读入;第四个参数为FILE*类型的指针,可以用上面打开文件时定义的变量。
例:用二进制读取结构体数据
#include<stdio.h> struct S { char name[20]; int age; float score; }; int main() { struct S s = { 0 }; //打开文件 FILE* pf = fopen("text.txt", "rb");//二进制此处是rb if (pf == NULL) { perror("fopen"); return 1; } //写文件 fread(&s, sizeof(s), 1, pf); printf("%s %d %.1f", s.name, s.age, s.score); //关闭文件 fclose(pf); return 0; }
这样我们就可以看见上面转义后的二进制数据成功又一次转义后的样式输出了出来,意思就是把文件中看不懂的数据转义后输出。
9.比较scanf/fscanf/sscanf, printf/fprintf/sprintf
1.scanf:按照一定的格式从键盘输入数据
printf:按照一定的格式把数据打印(输出)到屏幕上
适用于标准输入/输出流的格式化的输入/输出语句2.fscanf:按照一定的格式从输入流(文件/stdin)输入数据
fprintf:按照一定的格式向输出流(文件/stdout)输出数据
适用于所有输入/输出流的格式输入/输出语句3.sscanf:从字符串中按照一定的格式读取出格式化的数据
sprintf:把格式化的数据按照一定的格式转化为字符串通过结构体内数据对sscanf和sprintf解析:
#include struct S { char name[20]; int age; float score; }; int main() { char buf[100] = { 0 }; struct S tmp = { 0 }; struct S s = { "zhangsan",20,95.5f }; //能够把这个结构体的数据转换为字符串 sprintf(buf, "%s %d %.1f", s.name, s.age, s.score); printf("%s\n", buf); //将buf中的字符串,还原成一个结构体 sscanf(buf, "%s %d %f",tmp.name,&tmp.age,&(tmp.score)); printf("%s %d %.1f", tmp.name, tmp.age, tmp.score); return 0; }可以看出来,使用sprintf后,原来的数据类型就变成了字符串类型,需要使用%s输出,使用了sscanf后就可以将原来的字符串还原为原来的结构体类型,这就是这两个函数的妙用,可以适用于一些特殊场景的使用。
10.标准流的解析运用:
stdin--标准输入流--键盘
stdout--标准输出流--屏幕
stderr--标准错误流--屏幕这便是最基本的标准流运用:键盘输入,屏幕显示,不通过文件进行中转,但不能保存。
#include<stdio.h> int main() { int ch = fgetc(stdin); fputc(ch, stdout);//将ch的内容写在屏幕上 return 0; }
六、文件的随机读写
1、fseek
返回值为int类型接收,第一个用创建文件时需要的变量接收;第二个表示第几个数,就是你打算从这个文件中第几个数开始读起;第三个表示读取类型(3种),第一种:SEEK_SET,从头开始读取,是指从第二个数的位置为头开始,第二种:SEEK_CUR,从当前位置开始往后读取(只要使用了一次fputc,位置就会后移一个),第三种: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; }
seek_set我们从第一个开始写,写了九个,就是写入了This is a这九个字符(空格也算),然后再写入 sam四个字符,就把这四个字符写入了 ap的位置,就可以看到图中的样例了。
2、ftell:返回文件指针相对于起始位置的偏移量
就可以帮上面那个函数求出此时函数的位置了。
3、rewind:让文件指针返回起始位置,就是让其初始化,从头开始。
七、文件读取结束的判断
被错误使用的feof:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:fgetc 判断是否为 EOF .fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:fread判断返回值是否小于实际要读的个数。通过两个if判断,判断程序结束的原因时遇到文件结束,还是读取失败导致的报错。不能单通过报错就认为是读取失败或文件结束,应该更加全面分析。
#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); }
八、文件缓冲区
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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。