5. 对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
上面这些函数,我们已经了解过的有:scanf/printf、fscanf/fprintf。我们可以大致对它们进行归纳。
1) 适用于标准输入/输出流的格式化的输入/输出语句:
scanf:按照一定的格式从键盘输入数据。
printf:按照一定的格式把数据打印(输出)到屏幕上。
2) 适用于所有的输入/输出流的格式化输入/输出语句:
fscanf:按照一定的格式从输入流(文件/stdin)输入数据。
fprintf:按照一定的格式向输出流(文件/stdout)输出数据。
而剩下的sscanf/sprintf就有些特殊,让我们先了解一下它们:
sscanf
:从字符串中读取格式化数据。
int sscanf ( const char * s, const char * format, ...);
sprintf
:把格式化的数据写入字符串。
int sprintf ( char * str, const char * format, ... );
str
:被写入数据的字符串的地址。format
:格式化字符串。
光看函数形态还是比较模糊,还是得用样例来理解它们:
struct S { char name[20]; int age; float score; }; int main() { char buf[100] = { 0 };// 被写入数据的字符串 struct S s = { "灰太狼", 10086, 114.514f }; // 能否把这个结构体的数据,转化成字符串 // "灰太狼 10086 114.514" sprintf(buf, "%s %d %.3f", s.name, s.age, s.score);// 将格式化数据写入字符串 // 以字符串形式打印 printf("%s\n", buf);// 被写入格式化数据的字符串 // 能否将buf中的字符串还原成一个结构体数据? struct S tmp = { 0 };// 还原的结构体 sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score)); // 从字符串中读取数据到结构体tmp中 // 以结构体形式打印 printf("%s %d %.3f", tmp.name, tmp.age, tmp.score); return 0; }
运行结果:
总结一下:
sscanf:从字符串中按照一定的格式读取出格式化的数据。
sprintf:把格式化的数据按照一定的格式转化成字符串。
6. 文件的随机读写
上面我们学习了文件的顺序读写,但是我们能不能实现随机读写?
这当然是可以的,接下来介绍几个关于文件随机读写的函数。
6.1 fseek
根据文件指针的位置和偏移量来定位文件指针:
int fseek ( FILE * stream, long int offset, int origin );
stream
:文件指针。offset
:文件指针的偏移量。origin
:文件开始读写的位置,有SEEK_SET
(文件指针起始位置)、SEEK_CUR
(文件指针当前位置)、SEEK_END
(文件指针末尾位置)。
如果还不清晰,可以用下图来理解:
接下来,我们在用一个样例再次加深理解:
int main() { // 当前文件内容:abcdef FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen()"); return 1; } // 读文件 fseek(pf, 3, SEEK_SET);// 从起始位置偏移三个位置读取 int ch = fgetc(pf);// d printf("%c\n", ch); fseek(pf, -3, SEEK_END);// 从结尾位置向前偏移3个位置(相当于向后-3个位置)读取 ch = fgetc(pf);//d printf("%c\n", ch); // 经fgetc后,d被读取,文件指针偏移,当前文件指针指向e fseek(pf, 1, SEEK_CUR);// 从当前位置向后偏移1个位置读取 ch = fgetc(pf);// f printf("%c\n", ch); // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
6.2 ftell/rewind
ftell
返回文件指针相对于起始位置的偏移量:
long int ftell ( FILE * stream );
- stream:文件指针。
rewind
让文件指针的位置回到文件的起始位置:
void rewind ( FILE * stream );
下面样例中,为了测试ftell
,当我文件指针偏离起始位置很远时,再使用rewind
回到起始位置,并用ftell
测试功能,所以我将这两个函数放在一块讲解:
int main() { // 当前文件内容:abcdef FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen()"); return 1; } // 读文件 int ch = fgetc(pf);// 读取完指向b ch = fgetc(pf);// 读取完指向c ch = fgetc(pf);// 读取完指向d int pos = ftell(pf);// d相对于起始位置偏移量为3 printf("%d\n", pos); rewind(pf);// 回到起始位置a printf("%d\n", ftell(pf));// a相对于起始位置偏移量为0 // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
7. 文本文件和二进制文件
有些文件,我们打开可以看懂,但是有些文件就看不懂,就像这样:
看得懂:
看不懂:
根据数据的组织形式,我们将文件分成两类:
能看懂的称为文本文件
看不懂的称为二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
举个例子,如果10000以文本文件形式存储,那么就是将其的每一位看做字符,分别转化为ASCII值,以ASCII码值存入内存中。那么就是将49、48、48、48、48存入内存中。但是以二进制形式存储,就只需要把10000转化为二进制存入内存中,仅占用四个字节。
让我们用一个例子加深理解:
#include <stdio.h> int main() { int a = 10000; FILE* pf = fopen("test.txt", "wb"); fwrite(&a, 4, 1, pf);//二进制的形式写到文件中 fclose(pf); pf = NULL; return 0; }
我以二进制形式写入10000到文件中,而这时如果想要查看以二进制写入文件这个数据要占多少空间是看不出来的,因为我们无法看懂二进制文件:
于是我们使用另一种方式:
首先,将test.txt
添加进源文件:
然后右击,选择打开方式,以二进制编辑器方式打开:
随后,我们就可以看到10000在以二进制写入内存后,是如何存储的:
如果10000写成16进制的形式的话为:00 00 27 10,如果以小端字节序存储放入内存中(低字节内容放到低地址处,高字节内容放到高地址处),就为:10 27 00 00。
这里可以做出一个区分:
这里所说的文本文件,就是将数据以ASCII码值形式写进文件的意思,二进制文件就是将数据以二进制形式存储。而我们通常说的文本文件,就是以.txt为后缀的文件,千万不要混淆了。
8. 文件读取结束的判定
8.1 被错用的feof
feof
的作用在于文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
int feof ( FILE * stream );
feof返回的值为非0,说明是遇到了文件末尾结束。否则说明文件读取失败结束。
要知道一个文件在读取的过程中可能遇到io错误等,导致文件读取失败。或者读取到文件末尾,读取结束,这时就可以使用feof判断文件到底是读取结束的原因。
那么我们在日常写代码时,如何判断文件是否读取结束?
文本文件的读取结束判断:
fgetc如果读取正常,会返回读取到的字符的ASCII值,如果读取失败,会返回EOF。
fgets如果读取正常,返回的是存放读取到的数据的地址,如果读取失败,返回的是空指针。
fscanf如果读取正常,返回的是格式化字符串中指定的数据的个数,如果读取失败,返回的数字小于格式化字符串指定数据的个数
二进制文件的读取结束判断:
fread判断返回值是否小于实际要读的个数。
文本文件样例:
#include <stdio.h> #include <stdlib.h> int main() { int c; // 注意:int,非char,要求处理EOF FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("File opening failed"); return 1; } //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF while ((c = fgetc(pf)) != EOF) { putchar(c); } //判断是什么原因结束的 if (ferror(pf))// ferror返回值为真,说明遇到了I/O型错误 puts("I/O error when reading"); else if (feof(pf))// feof为真,成功读取到文件尾结束 puts("End of file reached successfully"); fclose(pf); pf = NULL; return 0; }
二进制文件样例:
#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// 读取不正常 { if (feof(fp)) printf("Error reading test.bin: unexpected end of file\n"); else if (ferror(fp)) perror("Error reading test.bin"); } fclose(fp); }
9. 文件缓冲区
可否记得,我们在将文件指针时,提到的缓冲文件系统?那么它该如何理解?
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
那么这句话这么生涩,该如何理解呢?我们通过一个样例来解读:
#include <stdio.h> int main() { int a = 10000; FILE* pf = fopen("test.txt", "wb"); fwrite(&a, 4, 1, pf);//二进制的形式写到文件中 fclose(pf); pf = NULL; return 0; }
我们将10000从内存写入文件中,需要经过什么步骤?它是直接写入的吗?
其实10000就相当于我们的内存(程序数据区),而test.txt就相当于我们的硬盘。在内存中有一块区域为输出缓冲区,当我们需要写入信息时,数据就会被放入输出缓冲区中,当输出缓冲区达到装满时,就会把数据输送到硬盘。同样的,当我们需要将硬盘上的数据输入到内存中时,会依托内存中的输入缓冲区,将数据输送到内存(程序数据区)中。
大概流程为这样:
由于将数据写入硬盘时,需要依托操作系统将数据写进硬盘。为了不让操作系统经常被打扰,于是就有了输出缓冲区,当输出缓冲区装满时,再调用操作系统,将数据一次性写入硬盘,这样效率也能提高。
但是这并不代表,只有装满输出缓冲区才能将数据写入硬盘。我们可以通过\n(行缓冲)、fflush(stdout)(强制刷新)、关闭文件等方法刷新缓冲区。
接下来,我们通过一个例子,感受缓冲区的存在:
int main() { FILE* pf = fopen("test.txt", "w"); fputs("abcdef", pf);//先将代码放在输出缓冲区 printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n"); Sleep(10000); printf("刷新缓冲区\n"); fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘) printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n"); Sleep(10000); fclose(pf); //注:fclose在关闭文件的时候,也会刷新缓冲区 pf = NULL; return 0; }
运行结果:
输出缓冲区未刷新:
输出缓冲区刷新:
运行结果:
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能会在不恰当的操作下导致数据的丢失,导致读写文件的问题。
10. 结语
到这里,本篇博客就到此结束了。相信大家对C语言文件操作也有了一定的了解。希望我的博客对您有帮助!
如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!
我是anduin,一名C语言初学者,我们下期见!