=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.com)
=========================================================================
接上期:
学C的第三十二天【动态内存管理】_高高的胖子的博客-CSDN博客
=========================================================================
- 以前面写的通讯录为例,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,
此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,
等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
- 既然是通讯录就应该把信息记录下来,
只有我们自己选择删除数据的时候,数据才不复存在。
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有:
把数据存放在磁盘文件、存放到数据库等方式。
- 使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
磁盘上的文件是文件。
但在程序设计中,平常讲的文件有两种(从文件功能角度分类):
程序文件 和 数据文件
(1). 程序文件:
包括:
- 源文件(后缀为 .c )
- 目标文件(windows环境后缀为 .obj )
- 可执行程序(windows环境后缀为 .exe )
(2). 数据文件:
数据文件的内容不一定是程序,而是程序运行时读写的数据,
比如程序运行需要从中读取数据的文件, 或者输出内容的文件。
这篇博客讨论的也是数据文件。
之前博客所处理数据的输入输出都是以终端为对象的,
即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,
当需要的时候再从磁盘上把数据读取到内存中使用,
这里处理的就是磁盘上的文件。
(3). 文件名:
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:
文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(1). 文件指针:
缓冲文件系统中,有个关键概念叫“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,
用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。
这些信息是保存在一个结构体变量中的。
该结构体类型是由系统声明的,取名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;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构体的变量,
并填充其中的信息, 使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构体的变量,这样使用起来更加方便。
可以创建一个FILE*的指针变量:
定义pf是一个指向FILE类型数据的指针变量。
FILE* pf;//文件指针变量
可以使用pf指向某个文件的文件信息区(是一个结构体变量)。
通过该文件信息区中的信息就能够访问该文件。
也就是说,通过文件指针变量能够找到与它关联的文件。
图解:
(2). 文件的打开和关闭:
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,
都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用 fopen函数 来打开文件,fclose函数 来关闭文件:
fopen函数的参数mode:
文件使用方式 含义 如果指定文件不存在 “r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错(返回NULL空指针) “w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件 “a”(追加) 向文本文件尾添加数据 建立一个新的文件 “rb”(只读) 为了输入数据,打开一个二进制文件 出错(返回NULL空指针) “wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件 “ab”(追加) 向一个二进制文件尾添加数据 建立一个新的文件 “r+”(读写) 为了读和写,打开一个文本文件 出错(返回NULL空指针) “w+”(读写) 为了读和写,建立一个新的文件 建立一个新的文件 “a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件 “rb+”(读写) 为了读和写,打开一个二进制文件 出错(返回NULL空指针) “wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件 “ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件 示例:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(1). 顺序读写函数介绍:
函数名 功能 适用于 fgetc 字符输入函数 所有输入流 fputc 字符输出函数 所有输出流 fgets 文本行输入函数 所有输入流 fputs 文本行输出函数 所有输出流 fscanf 格式化输入函数 所有输入流 fprintf 格式化输出函数 所有输出流 fread 二进制输入 文件 fwrite 二进制输出 文件
写(输出)和 读(输入) :
- “写(输出)” :
把程序的数据写(输出)到文件中
- “读(输入)” :
把文件的内容读取(输入)到程序中
图解:
-------------------------------------------------------------------------------------------------------------------------
示例:fputc 和 fgetc
fputc
- 第一个参数:
要输出的字符- 第二个参数:
指定(写入)输出的目标(输出流)//文件操作需要该头文件 #include
int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "w"); //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //顺序读写: //写文件: /*fputc('a', pf); fputc('b', pf); fputc('c', pf);*/ //也可以使用for循环写进文件: int i = 0; for (i = 0; i < 26; i++) { fputc('a' + i, pf); } //读文件: //关闭文件 fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
fgetc
- 第一个参数:
指定(读取)输入的位置(输入流)- 返回值:
读取到的字符,
因为字符的ASCII码值为数字,所以用int类型变量进行接收//文件操作需要该头文件 #include
int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "r"); //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //顺序读写: //读文件: int i = 0; for ( i = 0; i < 26; i++) { int ch = fgetc(pf); printf("%c", ch); } printf("\n"); //关闭文件 fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
-------------------------------------------------------------------------------------------------------------------------
示例:fputs 和 fgets
fputs
- 第一个参数:
要输出的一行字符串,如需换行要自己加"\n"- 第二个参数:
指定(写入)输出的目标(输出流)//文件操作需要该头文件 #include
int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "w"); //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //顺序读写: //写文件 - 写一行 fputs("hello world\n", pf); fputs("hello good good world\n", pf); // 第一个参数:要输出的一行字符串,换行要自己加 // 第二个参数:输出流,填写要输出的目标 //关闭文件 fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
fgets
- 第一个参数:
用来存放从输入流读取的一行数据的字符数组,
所以使用该函数时最好先准备一个字符串
- 第二个参数:
从输入流一行中读取的字符个数,
遇到换行符 "\n" 会提前结束读取,
需注意实际读取字符个数为 num-1 个,
能够读完的话最后一个字符会是换行符 "\n",不能读完则最后一个字符是结束符"\0"
所以使用该函数进行输入时,可以不用自己加换行符,
- 第三个参数:
指定(读取)输入的位置(输入流)//文件操作需要该头文件 #include
int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "r"); //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //顺序读写: //读文件 - 读一行 //读一行文件要先创建一个字符数组 //来存储待会从文件读到的数据: char arr[10] = { 0 }; //使用fgets函数读一行: fgets(arr, 10, pf); //打印读取到的数据: printf("%s", arr); //直接打印用于存储的字符串即可 //关闭文件 fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
-------------------------------------------------------------------------------------------------------------------------
示例:fprintf 和 fscanf
上面的示例中的四个函数是对字符类型进行文本操作,
这两个函数则可以对“带有格式”的数据(结构体)进行文本操作
fprintf
- 第一个参数:
跟printf函数相比,就多了这第一个参数,
指定(写入)输出的目标(输出流)- 第二个参数:
分别写出“带格式数据”的格式,
类似printf函数的第一个参数(%d、%s……)- 之后的参数:
之后的参数取决于自己需要加入多少个参数,
每个参数都需要在第二个参数中注明格式,
参数之间要用逗号隔开#include
struct S { int a; //整型数据 float s; //浮点型数据 }; int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "w"); //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //顺序读写: //写文件 - fprintf函数: struct S s = { 100, 3.14f }; //创建结构体变量 //使用fprintf函数输出带格式的数据: fprintf(pf, "%d %f", s.a, s.s); //关闭文件d fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
fscanf
- 第一个参数:
跟scanf函数相比,就多了这第一个参数,
指定(读取)输入的位置(输入流)- 第二个参数:
分别写出“带格式数据”的格式,
类似scanf函数的第一个参数(%d、%s……)- 之后的参数:
之后的参数取决于自己需要加入多少个参数,
每个参数都需要在第二个参数中注明格式,
参数之间要用逗号隔开,
类似scanf函数,需要加上取地址符&#include
struct S { int a; //整型数据 float s; //浮点型数据 }; int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "r"); //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //顺序读写: //读文件: struct S s = { 0 }; fscanf(pf, "%d %f", &(s.a), &(s.s)); //读取后放在结构体变量s中 //打印查看读取效果: printf("%d %f", s.a, s.s); //关闭文件d fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
-------------------------------------------------------------------------------------------------------------------------
示例:fwrite 和 fread
fwrite
- 第一个参数:
将要写入文件中的数据的起始地址(如数组名)- 第二个参数:
写到文件中的每个数据大小(如数组每个元素大小)- 第三个参数:
将要写入文件的数据个数(如数组元素个数)- 第四个参数:
指定(写入)输出的目标(输出流)#include
//fwrite: struct S { int a; float s; char str[10]; }; int main() { //创建一个结构体变量: struct S s = { 99, 6.18f, "hello" }; //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "wb"); //注:二级制写文件是“wb” //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //以二级制形式写文件: fwrite(&s, sizeof(struct S), 1, pf); //关闭文件d fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
fread
- 第一个参数:
给出一个地址(指针),
以二进制形式读取文件后,
把读取到的数据从该地址开始依次往后存放- 第二个参数:
读取(输入)的每个数据大小(如数组每个元素大小)- 第三个参数:
读取(输入)的数据个数(如数组元素个数)- 第四个参数:
指定(读取)输入的位置(输入流)- 返回值:
会返回读取到的数据个数,
可以根据返回值情况判断是否继续读取数据#include
//fwrite: struct S { int a; float s; char str[10]; }; int main() { //创建FILE类型指针,使用fopen函数进行文件操作 FILE* pf = fopen("data.txt", "rb"); //注:二级制读文件是“rb” //第一个参数文件名可以是相对路径或绝对路径 //操作失败可能会返回空指针,进行检验: if (pf == NULL) { //打印错误信息: perror("fopen"); return 1; } //创建一个结构体变量: //读取完二进制数据后存入改结构体变量中 struct S s = { 0 }; //以二级制形式读取文件: fread(&s, sizeof(struct S), 1, pf); //读取后打印查看读取情况: printf("%d %f %s\n", s.a, s.s, s.str); //关闭文件d fclose(pf); //这样只是文件关闭了 //还需要把文件指针置为空指针: pf = NULL; return 0; }
(2). 程序运行时的三个流:
在C语言程序中,只要程序运行起来,就默认会打开三个流:
- 标准输入流:stdin -- 可以使用 scanf、getchar 等函数
- 标准输出流:stdout -- 可以使用 printf、putchar 等函数
- 标准错误流:stderr
这三个流的类型都是 FILE* 指针
勿混淆:stdio.h
stdio.h 是一个头文件,指标准输入输出,
指的是从键盘上输入和把数据打印到屏幕上的这些函数的总和
补充:三组易混淆的函数
scanf 和 printf
- scanf -- 从标准输入流读取格式化的数据
- printf -- 向标准输出流写入格式化的数据
fscanf 和 fprintf
(上面有示例)
- fscanf -- 适用于所有输入流的格式化输入函数
- fprintf -- 适用于所有输出流的格式化输出函数
sscanf 和 sprintf
- sscanf -- 从字符串中读取格式化的数据(“把字符串转化为结构体变量数据”)
- sprintf -- 将格式化的数据转换为字符串(“把结构体变量数据转换为字符串”)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
随机读写:
随意指定文件的任意位置进行读写操作
当你使用文件指针打开对应文件的时候,
文件指针是指向文件起始位置的,所以要实现随机读写,
就要让文件指针指向你想要的位置
(1). fseek函数 :
跟函数可以根据文件指针的位置和偏移量来定位文件指针。
函数书写格式:
int fseek ( FILE * stream, long int offset, int origin );
对应参数和返回值描述:
- FILE * stream -- (参数一)接收要操作文件的文件指针
- long int offset -- (参数二)偏移量,正数向右偏移,负数向左偏移
- int origin -- (参数三)文件操作的起始位置,从哪开始偏移
有三个可选择的参数:
SEEK SET -- 从 文件的起始位置 开始偏移
SEEK CUR -- 从 当前文件指针的位置 开始偏移
SEEK END -- 从 文件的末尾位置 开始偏移
示例:
#include
int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = fgetc(pf); printf("%c\n", ch);//a ch = fgetc(pf); printf("%c\n", ch);//b ch = fgetc(pf); printf("%c\n", ch);//c //使用参数三的 SEEK CUR 进行演示: fseek(pf, -3, SEEK_CUR); ch = fgetc(pf); printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
(2). ftell函数 :
该函数会返回文件指针相对于起始位置的偏移量
函数书写格式:
long int ftell ( FILE * stream );
对应参数和返回值描述:
- FILE * stream -- (参数一)接收要操作文件的文件指针
- long int -- (返回值)返回文件指针相对于起始位置的偏移量
示例:
#include
int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = fgetc(pf); printf("%c\n", ch);//a ch = fgetc(pf); printf("%c\n", ch);//b ch = fgetc(pf); printf("%c\n", ch);//c //使用ftell函数当前偏移量: int pos = ftell(pf); printf("%d\n", pos); fclose(pf); pf = NULL; return 0; }
(3). rewind函数 :
让文件指针的位置回到文件的起始位置
函数书写格式:
void rewind ( FILE * stream );
对应参数和返回值描述:
- FILE * stream -- (参数一)接收要操作文件的文件指针
示例:
#include
int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = fgetc(pf); printf("%c\n", ch);//a ch = fgetc(pf); printf("%c\n", ch);//b ch = fgetc(pf); printf("%c\n", ch);//c //使用rewind函数让文件指针回到初始位置: rewind(pf); ch = fgetc(pf); printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存(文件、硬盘),
就是二进制文件。(直接打开进行查看是乱码)
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。
以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,
数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000:
如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),
而二进制形式输出,则在磁盘上只占4个字节(整型)。
(VS2013测试)
图解:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
被错误使用的feof函数 :
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
feof函数 的作用是:
当文件读取结束的时候,判断 遇到文件尾 是不是读取结束的原因,
读取结束还可能是因为读到中途遇到错误等等
1. 文本文件读取是否结束:
可以使用 feof函数 后,
判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
- 使用 fgetc函数 后 -- 使用 feof函数 判断返回值是否为 EOF,是则证明文件读取结束
- 使用 fgets函数 后 -- 使用 feof函数 判断返回值是否为 NULL,是则证明文件读取结束
2. 二进制文件的读取结束判断:
可以使用 fread函数 后,
判断返回值是否小于实际要读的个数。
例如:
- fread函数 会返回 实际读取个数 ,
所以可以通过判断 fread函数 返回值(实际读取个数)是否小于实际要读的个数,
是则说明二进制文件读取结束
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,
所谓缓冲文件系统,
是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,
装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,
则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),
然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
因为有缓冲区的存在,C语言在操作文件的时候,
需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
图解: