三、文件的顺序读写
0x00 什么是顺序读写
首先要了解什么是读写:我们写的程序是在内存中,而数据是要放到文件中的,文件又是在硬盘上的。当我们把文件里的数据读到内存中去时,这个动作我们称之为输入/读取。反过来,如果把程序中的东西放到硬盘上,这个动作我们称之为输出/写入。
📚 顺序读写,顾名思义就是按照顺序在文件中读和写。
0x01 顺序读写函数一览表
0x02 字符输出函数 fputc
📚 介绍:将参数 char 指定的字符写入到指定的流 stream 中,并把位置标识符向前移动 (字符必须为一个无符号字符)。适用于所有输出流。
💬 代码演示:创建一个 test.txt,随后使用 fputc 函数分别写入 "abc" 到文件中
#include <stdio.h> int main(void) { FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } // 写文件 fputc('a', pf); fputc('b', pf); fputc('c', pf); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩(代码正常运行)
📂 此时打开工程文件夹可以成功看到 test.txt 被创建了(大小为1kb可以看出写入成功了):
💬 我们正好测试下 w 的覆盖效果,我们把写的内容注释掉:
#include <stdio.h> int main(void) { FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } // 写文件 //fputc('a', pf); //fputc('b', pf); //fputc('c', pf); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 此时再次运行,我们发现那个文件里的内容不见了(大小也变为0kb):
❗ 值得注意的是,文件的写入是有顺序的。abc,先是a,然后是b,最后是c:
fputc('a', pf); fputc('b', pf); fputc('c', pf);
0x03 字符输入函数 fgetc
📚 介绍:从指定的流 stream 获取下一个字符,并把位置标识符向前移动(字符必须为一个无符号字符)。如果读取成功会返回相应的ASCII码值,如果读取失败它会返回一个EOF。适用于所有输入流。
💬 代码演示:在工程文件夹里打开 test.txt ,随便写入一些数据,随后使用 fgetc 函数读取:
#include <stdio.h> // 使用fgetc从文件里读 int main(void) { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } // 读文件 int ret = fgetc(pf); printf("%c\n", ret); ret = fgetc(pf); printf("%c\n", ret); ret = fgetc(pf); printf("%c\n", ret); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 运行结果如下:
❓ 如果读完了会发生什么?
🐞 我们来调试一下看看:
0x04 文本行输出函数 fputs
📚 介绍:将字符串写入到指定的流 stream 中(不包括空字符)。适用于所有输出流。
💬 代码演示:利用 fputs 在 test2.txt 中随便写入几行数据:
#include <stdio.h> int main(void) { FILE* pf = fopen("test2.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } // 写文件 - 按照行来写 fputs("abcdef", pf); fputs("123456", pf); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 (代码成功运行)
❓ 如果想换行呢?
🔑 换行需要在代码里自行加 \n :
fputs("abcdef\n", pf); fputs("123456", pf);
(这时候打开文件,就是换行的了)
0x05 文本行输入函数 fgets
📚 介绍:从指定的流 stream 读取一行,并把它存储在 string 所指向的字符串中,当读取(n-1)个字符时,或者读取到换行符、到达文件末尾时,它会停止,具体视情况而定。适用于所有输入流。
📌 注意事项:假如 n 是100,读取到的就是99个字符(n-1),因为要留一个字符给斜杠0。
💬 代码演示:利用 fgets 读取 test2.txt 中的内容:
#include <stdio.h> int main(void) { char arr[10] = "xxxxxx"; // 存放处 FILE* pf = fopen("test2.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } // 读文件 - 按照行来读 fgets(arr, 4, pf); printf("%s\n", arr); fgets(arr, 4, pf); printf("%s\n", arr); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 代码运行结果如下:
🐞 调试一下看看:
0x06 格式化输出函数 fprintf
📚 介绍:fprintf 用于对格式化的数据进行写文件,发送格式化输出到流 stream 中。适用于所有输出流。
💬 代码演示:将结构体的三个数据利用 fprintf 写到 test3.txt 中:
#include struct Player { char name[10]; int dpi; float sens; }; int main(void) { struct Player p1 = { "carpe", 900, 3.12f }; // 对格式化的数据进行写文件 FILE* pf = fopen("test3.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } // 写文件 fprintf(pf, "%s %d %f", p1.name, p1.dpi, p1.sens); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 (代码成功运行)
0x07 格式化输入函数 fscanf
📚 介绍:fscanf 用于对格式化的数据进行读取,从流 stream 读取格式化输入。适用于所有输入流。
💬 代码演示:利用 fscanf 读取 test4.txt 中的内容,并打印:
#include struct Player { char name[10]; int dpi; float sens; }; int main(void) { struct Player p1 = { 0 }; // 存放处 // 对格式化的数据进行写文件 FILE* pf = fopen("test3.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } // 读文件 fscanf( pf, "%s %d %f", p1.name, &(p1.dpi), &(p1.sens) ); // 👆 p1.name本身就是地址(不用&) // 将读到的数据打印 printf("%s %d %f\n", p1.name, p1.dpi, p1.sens); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 运行结果如下:
0x08 二进制输出函数 fwrite
📚 介绍:写一个数据到流中去,把 buffer 所指向的数组中的数据写入到给定流 stream 中。
💬 创建一个 test5.txt,用 fwrite 写入一个数据到 text5.txt 中去:
#include // 二进制的形式写 struct S { char arr[10]; int num; float score; }; int main(void) { struct S s = { "abcde", 10, 5.5f }; FILE* pf = fopen("test5.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } // 写文件 fwrite(&s, sizeof(struct S), 1, pf); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 (代码成功运行)
我们发现他烫起来了2333(划掉)
💡 打开文件后我们发现只有abcde看得懂,后面是什么我们看不懂。
我们试着用 nodepad++ 打开:
❓ 为什么还是乱码?为什么 abcde 不是乱码?
🔑 解答:
① 我们刚才用的都是文本编译器,文本编译器打开二进制形式的文件完全是两种状态。
② 因为字符串以文本形式写进去和以二进制形式写进去是一样的,但是对于整数、浮点数等来说就不一样了,文本形式写入和二进制形式写入完全是两个概念。
(那么该怎么读呢,我们来看下面的 fread 函数)
0x09 二进制输入函数 fread
📚 介绍:从流中读取,从给定流 stream 读取数据到 buffer 所指向的数组中。
💬 用 fread 读取 text5.txt 中的二进制数据:
#include // 二进制的形式读 struct S { char arr[10]; int num; float score; }; int main(void) { struct S s = { 0 }; // 存放处 FILE* pf = fopen("test5.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } // 读文件 fread(&s, sizeof(struct S), 1, pf); // 将读到的数据打印 printf("%s %d %f", s.arr, s.num, s.score); // 关闭文件 fclose(pf); pf = NULL; return 0; }
🚩 (代码正常运行)
🔺 总结: fwrite 和 fread 是一对,fwrire 写进去用 fread 读。
0xA0 流的概念(stream)
在这里,我们补充一下流的概念。
📚 观察刚才的表格我们可以发现有的函数是适用于所有xx流的(比如 fputc 函数)。fputc 就适用于所有输出流,也就是说它不仅仅可以给文件里写。我们来读一下MSDN的介绍:
我们发现它还可以写到 stdout 上。
❓ 那么 stdout 是什么呢?
💡 stdout 就是标准输出流,在这里,我们要来讲一下流的概念。
📚 C语言默认打开的3个流:
① stdin - 标准输入流 - 键盘
② stdout - 标准输出流 - 屏幕
③ stderr - 标准输出流 - 屏幕
💬 我们用流向屏幕上输出信息 - stdout:
#include int main(void) { fputc('a', stdout); fputc('b', stdout); fputc('c', stdout); return 0; }
🚩 a b c
💬 fgetc 从标准输入流读取 - stdin
#include // 使用fgetc从标准输入流中读 int main(void) { int ret = fgetc(stdin); printf("%c\n", ret); ret = fgetc(stdin); printf("%c\n", ret); ret = fgetc(stdin); printf("%c\n", ret); return 0; }
🚩 运行: