4. 文件的顺序读写
顺序读写,即按照文件中信息的顺序,逐次读写。
但是说到读写,它到底是什么意思?
之前我们只了解printf
和scanf
函数,scanf是从外部设备(键盘),输入(读取)数据到内存中,为读。printf是从内存中,输出数据到外部设备(屏幕)上,为写。
而文件的读,是从文件中读取(输入)信息,到内存中;文件的写,是由内存向文件写入(输出)数据。
4.1 函数总览
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输入函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fcanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwirte | 文件 |
4.2 fputc
向文件中写入一个字符:
int fputc ( int character, FILE * stream );
character
:写入的字符stream
:相关文件的文件流(即字符指针)。
返回值为字符的ascii码值,所以类型为int。
接下来我们使用循环来向文件中写入26个英文字母:
int main() { // 打开文件 FILE* pf = fopen("test.txt", "w");// 写 if (NULL == pf) { perror("fopen"); return 1; } // 向文件中写入26个英文字母 for (int i = 0; i < 26; i++) { fputc('a' + i, pf);// 通过循环得到所有的小写字母字符 } // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
4.3 fgetc
向文件中读取一个字符:
int fgetc ( FILE * stream );
stream
:相关文件的文件流(即字符指针)。
返回值为字符的ascii码值,所以类型为int。
我们上次已经将26个英文字母写入了文件中,那么我们再将它读出来:
int main() { // 打开文件 FILE* pf = fopen("test.txt", "r");// 读 if (NULL == pf) { perror("fopen"); return 1; } // 向文件中读取26个英文字母 for (int i = 0; i < 26; i++) { int ch = fgetc(pf);// 将读到的字符的ascii码值放到ch中 printf("%c ", ch); } // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
但是如果我们不知道文件中到底有多少个字母,我们该如何读取?
其实在函数介绍的返回值部分,还有一点需要补充fgetc读取失败返回EOF。
所以,纠正一下:fgetc读取成功返回字符的ascii码值,读取失败返回EOF。
所以我们可以改写成这样的形式:
int main() { // 打开文件 FILE* pf = fopen("test.txt", "r");// 读 if (NULL == pf) { perror("fopen"); return 1; } int ch = 0; while ((ch = fgetc(pf)) != EOF)// fgetc返回值不等于EOF则继续读取 { printf("%c ", ch); } // 关闭文件 fclose(pf); pf = NULL; return 0; }
4.4 fputs
从内存中写入数据到文件中:
int fputs ( const char * str, FILE * stream );
str
:写入文件的字符串。stream
:相关文件的文件流(即字符指针)。
fputs
是按照顺序写入的,只关注写入的内容,不考虑换行。
将字符串写入文件中:
// 按照顺序写入文本行 int main() { // 打开文件 FILE* pf = fopen("test.txt", "w");// 写 if (NULL == pf) { perror("fopen"); return 1; } // 写文件,按行写入 fputs("hello\n", pf);// 手动写入换行 fputs("world\n", pf);// 手动写入换行 // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
4.5 fgets(精讲)
向文件中按行读取数据到内存中:
char * fgets ( char * str, int num, FILE * stream );
str:存放读取数据的位置(读取的数据会被存入该空间中)。
num:读取字符的最大数。
stream:相关文件的文件流(即字符指针)。
那么fgets函数有没有什么细节?使用的时候我们需要注意什么?我们接下来一一观察:
(注:当前test.txt文件中依然存放着fputs写入的数据)
int main() { // 打开文件 FILE* pf = fopen("test.txt", "r");// 读 if (NULL == pf) { perror("fopen"); return 1; } // 读文件,按行读取 char arr[20] = "##########"; fgets(arr, 3, pf); // 关闭文件 fclose(pf); pf = NULL; return 0; }
当我num = 3
时,只读取了2个字符。这说明当读取的最大字符数小于文本行数据的总长度时,fgets
实际上只会读取num - 1
个数据,还有一个为填充的’\0’!!!
当我们本行数据没有读取完成,下一次读取,仍然会读取这一行,直到本行读取完毕才会读取下一行:
运行结果:
而当我们读取的最大字符数大于文本行数据的总长度时(例如我要读取10个字符,但是我的文本行只有hello),会发生什么?
我们发现当最大字符数大于文本行数据的总长度时,本行数据被读取完毕,停止读取,在字符串末尾加上’\0’,且不会读取下一行。
那么读取两行文本行怎么做?
int main() { // 打开文件 FILE* pf = fopen("test.txt", "r");// 读 if (NULL == pf) { perror("fopen"); return 1; } // 读文件,按行读取 char arr[20] = "##########"; fgets(arr, 7, pf); printf("%s", arr); fgets(arr, 7, pf); printf("%s", arr); // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
注意点:若fgets
读取数据到字符指针中一定要初始化!!!
例如:
char* p;// 一定要初始化,否则没有空间来存放数据
我们可以使用动态内存为其分配空间:
char* p = (char*)malloc(sizeof(int) * 20);
使用:
#include <stdlib.h> int main() { // 打开文件 FILE* pf = fopen("test.txt", "r");// 读 if (NULL == pf) { perror("fopen"); return 1; } // 读文件,按行读取 char* p = (char*)malloc(sizeof(int) * 20);// 动态分配内存 fgets(p, 7, pf); printf("%s", p); fgets(p, 7, pf); printf("%s", p); // 关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
总结一下fgets在使用时的注意点:
当最大字符数count小于文本行数据的总长度时,fgets实际上只会读取num - 1个数据,还有一个为填充的’\0’。
当最大字符数count大于文本行数据的总长度时,本行数据被读取完毕,停止读取,在字符串末尾加上’\0’,且不会读取下一行。
fgets在未完全读取一行元素后,下一次读取会先读取未读取完的这一行。本行读取完毕开始读取下一行。
当str参数为指针时,需要初始化。
当fgets读取失败时,会返回NULL空指针。
4.6 fprintf
前面的fgec、fgets
分别对应着字符和字符串的操作,但是格式化数据如何写入文件?这时就要说起fprintf
函数。
把格式化数据从内存写入文件中:
int fprintf ( FILE * stream, const char * format, ... );
stream
:相关文件的文件流(即字符指针)。format
:格式化字符串
其实对比printf
我们就多了一个stream
参数,只需要加上一个文件指针即可。
比如我们向文件中写入一个结构体:
struct S { char name[20]; int age; float score; }; int main() { struct S s = { "灰太狼", 10086, 114.514f }; // 把s中的数据写到文件中 FILE* pf = fopen("test.txt", "w");// 写 if (NULL == pf) { perror("fopen"); return 1; } // 写文件 fprintf(pf, "%s %d %f", s.name, s.age, s.score); fclose(pf); pf = NULL; return 0; }
运行结果:
4.7 fscanf
把格式化数据从文件读取到内存中:
int fscanf ( FILE * stream, const char * format, ... );
stream
:相关文件的文件流(即字符指针)。format
:格式化字符串。
fscanf对比scanf也是多了一个文件指针。
例如将文件中的内容读取到内存中:
(注:当前test.txt
文件中依然存放着fprintf
写入的数据)
struct S { char name[20]; int age; float score; }; int main() { struct S s = { "灰太狼", 10086, 114.514f }; // 把s中的数据写到文件中 FILE* pf = fopen("test.txt", "r");// 读 if (NULL == pf) { perror("fopen"); return 1; } // 读文件 fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score)); printf("%s %d %f\n", s.name, s.age, s.score); fclose(pf); pf = NULL; return 0; }s
运行结果:
4.8 fwrite
以二进制形式向文件中写入数据:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
ptr
:被写入元素的起始地址。size
:写入元素的大小。count
:写入元素的个数。stream
:相关文件的文件流(即字符指针)。
通俗讲就是,从ptr
开始的数据,一次写入count
个size
大小的元素到文件中。
例如我们以二进制形式向文件中写入一个结构体的数据:
struct S { char name[20]; int age; float score; }; int main() { struct S s = { "灰太狼", 10086, 114.514f }; FILE* pf = fopen("test.txt", "wb");// 二进制只写 if (NULL == pf) { perror("fopen"); return 1; } // 从s的地址处,以二进制形式向文件中写入一个大小为s的元素 fwrite(&s, sizeof(s), 1, pf); fclose(pf); pf = NULL; return 0; }
运行结果:
里面有些数据是看不懂的,因为我们是以二进制写入的。
4.9 fread
以二进制形式从文件中读取数据到内存中:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
ptr:存放读取数据的空间的起始地址。
size:读取数据的大小。
count:读取数据的个数。
stream:相关文件的文件流(即字符指针)
fread的返回值:读取正常返回size的大小;读取失败返回实际读到的完整元素的个数(小与size)。
通俗讲就是,从文件流中读取count个大小为size的数据,到ptr指向的空间中。
(注:当前test.txt文件中依然存放着fwirte写入的数据)
struct S { char name[20]; int age; float score; }; int main() { struct S s = { "灰太狼", 10086, 114.514f }; FILE* pf = fopen("test.txt", "rb");// 二进制只读 if (NULL == pf) { perror("fopen"); return 1; } fread(&s, sizeof(s), 1, pf); printf("%s %d %f\n", s.name, s.age, s.score); fclose(pf); pf = NULL; return 0; }
运行结果:
虽然我们肉眼看不懂二进制形式的数据,但是使用程序,以二进制形式读取这些数据到屏幕上,还是可以看懂的。
4.10 补充知识(重要)
在函数总览的表中,我们看到了上述函数适用于什么流。我们当前只看到上述函数适用于文件,但是它们是否能适用于其他流?
我们在操作文件时,需要打开或关闭文件。但是我们平常使用printf、scanf函数时,并没有打开键盘和屏幕。这是因为我们的屏幕和键盘是默认打开的,而文件则不是默认打开的。
键盘、屏幕、文件均为外部设备。
对任何一个C程序,只要运行起来,就默认打开3个流:
stdin:标准输入流 - 键盘
stdout:标准输出流 - 屏幕
stderr:标准错误流 - 屏幕
而上述流的类型都是**FILE***的。
那么,既然我们的标准输入流stdin是FILE*,那么这是否说明,也能被上述函数使用?
举个例子,我们的fgetc和fputc函数是这样的形式:
int fgetc ( FILE * stream );
int fputc ( int character, FILE * stream );
它们的参数有FILE*
类型,那么我们将stdin
传入fgetc
,将stdout
传入fputc
:
int main() { int ch = fgetc(stdin); fputc(ch, stdout); return 0; }
运行结果:
那么通过这个样例,我们也可以推导出:
scanf(...) <==> fscanf(stdin, ...); ----------------------------------- printf(...) <==> fprintf(stdout, ...);
同样的我们也可以举出样例:
int main() { int arr[] = { 1,2,3,4,5 }; int sz = sizeof(arr) / sizeof(arr[0]); // 输入 for (int i = 0; i < sz; i++) { fscanf(stdin, "%d", &arr[i]); } // 输出 for (int i = 0; i < sz; i++) { fprintf(stdout, "%d ", arr[i]); } return 0; }
运行结果:
4.11 杂例(选读)
这是博主探究fgets
和fputs
时,偶然发现的一个问题。就博主看来知识点与本篇博客有些不符,但是秉持让博客内容更加丰富的原则,加上这个问题也是博主踩过的一个坑,加上探究过程也是一波三折,于是将其作为选读,记录在本博客中,和大家一起分享!
当我在工程的目录下,手动创建一个test.txt
,然后往里面写入中文:圣光背叛了我!
当我在程序中,使用只读的方式打印的是乱码:
int main() { FILE* pf = fopen("test.txt", "r"); if (NULL == pf) { perror("fopen"); return 1; } char arr[20] = "###########"; fgets(arr, 20, pf); printf("%s", arr); fclose(pf); pf = NULL; return 0; }
运行结果:
但是如果我先用fputs
将这句话重新写入文件,再以fgets
的方式取出时,就是正常的:
int main() { FILE* pf = fopen("test.txt", "r"); if (NULL == pf) { perror("fopen"); return 1; } char arr[20] = "###########"; //fputs("圣光背叛了我!\n", pf);// 以fputs写入 fgets(arr, 20, pf); printf("%s", arr); fclose(pf); pf = NULL; return 0; }
运行结果:
这是为什么?
这里其实是一个字符编码集的问题。我们平常的设备默认的字符编码集是UTF - 8的。但是我们从程序里面读取数据的时候,放到运行的黑框中,它的默认显示的字符集是GB2312的。然后就导致它们两个的字符集不一样了。写入的字符集和打印输出的字符集不同,就显示就乱码了。
而我们用fputs写入用fgets读取是正常的是因为它们的字符集是一样的,不存在写入的软件和读取的软件的程序字符集不一样的情况。
然后博主就开始了尝试…
我是直接更改本文件的字符集的,在vs2022中大体步骤为:
编辑区找到工具 -> 自定义 -> 命令 -> 菜单栏下拉找到文件 -> 添加命令 -> 类别中找到文件 -> 命令中下拉找到高级保存选项 -> 点击确定 -> 关闭自定义任务栏
编辑区找到文件 -> 经过以上操作后会出现高级保存选项 -> 再打开将编码从简体中文(GB2312)改为UTF - 8
注:编辑区就是vs2022中最上面一条栏,有文件,编辑等…
由于这是一篇文件相关的操作,所以只列出最后一步,过程就不一一截图展示操作了,有兴趣的可以上网查找操作。
然后查看本电脑字符集是否为UTF - 8,不是的需要重启更改一下。
更改完毕就可以重新开始测试了,这时将文件内容手动输入为圣光背叛了我,无需使用fputs进行写入,直接使用fgets就可以读取到文件的内容。因为这时,电脑字符集和程序显示的字符集相同了。
int main() { FILE* pf = fopen("test.txt", "r"); if (NULL == pf) { perror("fopen"); return 1; } char arr[20] = "###########"; fgets(arr, 20, pf); printf("%s", arr); fclose(pf); pf = NULL; return 0; }
运行结果:
注:这里博主电脑的默认字符集其实不是UTF - 8。在测试时,由于字符集不同,所以重启后测试时,可能是核心文件找不到的原因,导致一直跳弹窗,很令人苦恼,而且博主还测试错了好几遍,当时简直快奔溃了…如果你电脑的默认字符集不是UTF - 8的话,谨慎尝试,很烦,真的很烦!!!这也是为什么我将这个杂例设定为选读的原因…至于这个知识点,还是了解就好~