5、fscanf 与 fprintf
函数功能
fscanf:把文件中的数据格式化的读取到内存中;fprintf:把内存中的数据格式化的写入到文件中;
函数参数
int fscanf( FILE *stream, const char *format [, argument ]... ); # stream 对应文件指针 # format 格式控制字符串 # argumeng 可选参数 # int 函数返回值,成功时,函数返回成功填充的参数列表的项数,读取失败或者遇到文件末尾返回EOF int fprintf( FILE *stream, const char *format [, argument ]... ); # 参数和fscanf相同 # int 函数返回值,成功时,返回写入的字符总数,失败时返回一个负数
函数使用
fscanf 和 fprintf 的使用与 scanf 以及 printf 函数的使用基本相同,只是多了一个文件指针参数而已。
struct Stu { char name[20]; int age; char sex[10]; }; int main() { struct Stu stu = { "zhangsan", 20, "nan" }; //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //向文件中格式化写入数据 fprintf(pf, "%s %d %s", stu.name, stu.age, stu.sex); //关闭文件 fclose(pf); pf = NULL; return 0; }
struct Stu { char name[20]; int age; char sex[10]; }; int main() { struct Stu stu = { 0 }; //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //从文件中格式化读取数据 fscanf(pf, "%s %d %s", stu.name, &(stu.age), stu.sex); printf("%s %d %s\n", stu.name, stu.age, stu.sex); //关闭文件 fclose(pf); pf = NULL; return 0; }
6、fwrite 与 fread
函数功能
fwrite:以二进制的形式向文件中写入数据;fread:以二进制的形式从文件中读取数据;
函数参数
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ); # ptr 指向要写入的空间的起始地址 # size 要写入的每个元素的大小 # count 要写入的元素的个数 # stream 对应文件指针 # size_t 函数返回值,返回成功写入的元素总数 size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); # 参数与 fwrite 相同 # size_t 函数返回值,返回成功读取的元素总数
函数使用
struct Stu { char name[20]; int age; char sex[10]; }; int main() { struct Stu stu = { "zhangsan", 20, "nan" }; //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //以二进制的形式向文件中格式化写入数据 fwrite(&stu, sizeof(struct Stu), 1, pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
struct Stu { char name[20]; int age; char sex[10]; }; int main() { struct Stu stu = { 0 }; //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //以二进制的形式从文件中格式化读取数据 fread(&stu, sizeof(struct Stu), 1, pf); printf("%s %d %s\n", stu.name, stu.age, stu.sex); //关闭文件 fclose(pf); pf = NULL; return 0; }
7、拓展:sscanf 与 sprintf
曾经在校招时曾考过这样一道题,说出 scanf/fscanf/sscanf 和 printf/fprintf/sprintf 这三组函数如何使用以及它们之间的区别与联系,所以这里我们扩展学习一下 sscanf 和 sprintf 函数。
函数功能
sscanf:将一个字符串中的数据格式化;sprintf:将一组格式化的数据转换为字符串;
函数参数
int sscanf( const char *buffer, const char *format [, argument ] ... ); # buffer 用于存储数据的字符串的首地址 # format 格式控制字符串 # argumeng 可选参数 # int 函数返回值,成功时,函数返回成功填充的参数列表的项数,失败返回EOF int sprintf( char *buffer, const char *format [, argument] ... ); # 参数与 sscanf 函数相同 # int 函数返回值,成功时,返回写入的字符总数,失败则返回一个负数
函数使用
struct Stu { char name[20]; int age; char sex[10]; }; int main() { struct Stu stu = { "zhangsan", 20, "nan" }; char str[50]; //定义一个字符数组,用来存储从结构体中读取的数据 //将一组格式化的数据转换为字符串 sprintf(str, "%s %d %s", stu.name, stu.age, stu.sex); printf("%s\n", str); //将一个字符串中的数据格式化 struct Stu stu1 = { 0 }; sscanf(str, "%s %d %s", stu1.name, &(stu1.age), stu1.sex); printf("%s %d %s\n", stu1.name, stu1.age, stu1.sex); return 0; }
scanf / fscanf / sscanf 三者的区别与联系
scanf:scanf 函数是格式化输入函数,只适用于标准输入流(键盘、屏幕);
fscanf:scanf 函数也是格式化输入函数,不过它适用于所有输入流;
sscanf:sscanf 是专门针对字符串操作函数,用于将字符串数据转换为格式化的数据;
prinft / fprintf / sprintf 三者的区别与联系
prinft :prinft 函数是格式化输出函数,只适用于标准输入流(键盘、屏幕);
fprintf: fprintf 函数也是格式化输出函数,不过它适用于所有输入流;
sprintf :sprintf 是专门针对字符串操作函数,用于格式化的数据转化为字符串;
三、文件的随机读写
在上面我们介绍了文件顺序读写的相关函数及其操作,但是要高效率的使用文件中的数据,只会顺序读写显然是不够的,我们还得学会文件的随机读写。
1、fseek
根据文件指针的位置和偏移量来定位文件指针,即通过给定偏移量以及偏移量的参考位置来将文件指针移动到指定位置。
函数参数
int fseek( FILE *stream, long offset, int origin ); # stream 对应文件指针 # offset 相对于origin参数的偏移量 # origin 偏移量的参考位置 # int 函数返回值,设置成功返回0,否则返回非0
origin 参数的可能取值
-数值 | -参考位置 |
SEEK_SET | 文件的起始位置 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的结尾 |
函数使用
int main() { //以写的形式打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //将字符 'a' - 'z' 写到文件中去 int i = 0; for (i = 'a'; i <= 'z'; i++) { fputc(i, pf); } //关闭文件 fclose(pf); pf = NULL; //以读的形式打开文件 FILE* pf1 = fopen("test.txt", "r"); if (pf1 == NULL) { perror("fopen"); return 1; } //设置文件指针指向最后一个字符 fseek(pf1, -1, SEEK_END); //读出该字符 char ch = fgetc(pf1); printf("%c\n", ch); //关闭文件 fclose(pf1); pf1 = NULL; return 0; }
2、ftell
函数功能
返回文件指针相对于起始位置的偏移量。
函数参数
long int ftell ( FILE * stream ); # stream 对应函数指针 # long int 函数返回值,正常时返回函数指针相对于起始位置的偏移量,出错时返回-1L
函数使用
int main() { //以读的形式打开文件 FILE* pf1 = fopen("test.txt", "r"); if (pf1 == NULL) { perror("fopen"); return 1; } //设置文件指针指向最后一个字符 fseek(pf1, -1, SEEK_END); //求出文件指针相对于起始位置的偏移量 int ret = ftell(pf1); printf("%d\n", ret); //关闭文件 fclose(pf1); pf1 = NULL; return 0; }
3、rewind
函数功能
让文件指针的位置回到文件的起始位置。
函数参数
void rewind ( FILE * stream ); # stream 对应文件指针
函数使用
int main() { int n; FILE* pFile; char buffer[27]; //以读写的方式打开文件 pFile = fopen("test.txt", "w+"); //将字符 'A' - 'Z' 写入到文件中 for (n = 'A'; n <= 'Z'; n++) fputc(n, pFile); //使文件指针回到文件起始位置 rewind(pFile); //读数据 fread(buffer, 1, 26, pFile); buffer[26] = '\0'; puts(buffer); //关闭文件 fclose(pFile); pFile = NULL; return 0; }
四、文件的进阶知识
1、文本文件和二进制文件
根据数据的组织形式,数据文件被分为文本文件和二进制文件;
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件;如果要求在外存上以ASCII码的形式存储,则是文本文件,以ASCII码的形式需要在存储前进行转换。
我们以整数10000为例,我们知道,10000的二进制序列为 00000000 00000000 00100111 00010000,如果我们以二进制文件的形式存储,那么文件中的数据和内存中的数据一样,占四个字节;如果我们以文本文件的形式存储,那么10000就会被看作为5个字符 ‘1’ ‘0’ ‘0’ ‘0’ ‘0’,然后转化为对应的ASCII码存储 ,占五个字节;
2、文件读取结束的判定
feof 与 ferror
函数功能
当文件读取结束时,判断读取失败的原因。
函数参数
int feof( FILE *stream ); # stream 定义文件指针 # int 函数返回值,如果当前位置是文件末尾,返回非0,否则返回0 int ferror( FILE *stream ); # int 函数返回值,如果当前位置读取发生错误,返回非0,无错误返回0
被错误使用的feof
在文件的使用中,feof 函数的返回值常被错误的认为是用来判断文件是否读取结束的,其实,在文件读取过程中,feof 函数的返回值并不能直接用来判断文件是否结束,而是应当用于在文件读取结束的时候,判断是因为读取失败而结束,还是因为遇到文件尾而结束;
判断文件是否读取结束应该采用如下方法:
- 若要检查文本文件读取是否结束,判断返回值是否为 EOF (fgetc),或者是否为 NULL (fgets);
- 若要检查二进制文件的读取是否结束,判断返回值是否小于实际要读的个数;
feof 函数的正确使用
文本文件的例子:
int main(void) { int c; //把c定义为int,使其有能力接受EOF(-1) 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(fp)为真,说明由读取失败引起 puts("I/O error when reading"); else if (feof(fp)) //feof(fp)为真,说明由遇到文件尾引起 puts("End of file reached successfully"); fclose(fp); fp = NULL; }
二进制文件的例子:
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); // 向文件中以二进制的形式写入一个数组的数据 fclose(fp); fp = NULL; double b[SIZE]; fp = fopen("test.bin", "rb"); size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 从文件中读取二进制的数据 if (ret_code == SIZE) { //如果fread函数的返回值等于参数SIZE,说明正常读取 puts("Array read successfully, contents: "); for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]); putchar('\n'); } else //否则,遇到错误或者文件尾 { if (feof(fp)) //表示遇到文件尾 printf("Error reading test.bin: unexpected end of file\n"); else if (ferror(fp)) //表示遇到错误 perror("Error reading test.bin"); } fclose(fp); fp = NULL; }
3、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理数据文件,所谓缓冲文件系统是指系统自动的在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”;从内存向磁盘输出数据时会先送到内存中的缓冲区,等到装满缓冲区后再一起送到磁盘上;如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,等到装满缓冲区后,再从缓冲区逐个地将数据送到程序数据区(程序变量等);缓冲区的大小由C编译系统决定。
注意事项
文件缓冲区存在的原因:当我们使用 fwrite 等函数向文件中写入或者读取数据的时候,其实这些函数首先会调用系统调用,而系统调用是由操作系统提供的接口,所以写文件的操作其实最终是由操作系统来完成的;而如果我们不设置文件缓冲区,频繁的打断操作系统,让它来为我们写入、读取数据,则势必会降低操作系统的工作效率;所以设立文件缓冲区是为了提高操作系统的工作效率;
既然只有将文件缓冲区填满后才会进行写入、读取数据,那么当我们数据非常小,不足以填满文件缓冲区的时候是不是就会发生错误呢?其实不是的,我们每次文件操作完毕后都会使用 fclose 函数来关闭文件,而fclose 函数的内部会自动执行 fflush (刷新缓冲区) 操作,所以不必担心数据过小而操作失败,这也侧面反映了如果我们在使用文件之后不对文件进行关闭的话可能会导致文件的读写问题;
文件缓冲区的例子:
#include <stdio.h> #include <windows.h> //VS2019 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; }