目录
一.为什么使用文件
二.什么是文件
1.程序文件和数据文件
2.文件名
三.文件的打开和关闭
1.文件指针
2.fopen函数和fclose函数
四.文件的顺序读写
1.顺序读写函数一览表
2.主要输入输出函数介绍
(1)字符输出函数futc
(2)字符输入函数fgetc
(3)文本行输出函数 fputs
(4)文本行输入函数 fgets
(5)格式化输出函数 fprintf
(6)格式化输入函数 fscanf
(7)二进制输出函数 fwrite
(8)二进制输入函数 fread
五.文件的随机读写
1.文件指针定位函数 fseek
2.返回偏移量函数 ftell
3.文件指针回到起始位置函数 rewind
六.流 的概念
七.文本文件和二进制文件
1.文本文件
2.二进制文件
3.数据在文件中的存储方式
八.文件读取结束的判定
1.经常被错误使用的“feof”函数
2.正确判定文件是否读取结束的方法
九.文件缓冲区
1.什么是文件缓冲区
2.冲刷缓冲区函数 fflush
一.为什么使用文件
引言:
- 我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
- 我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
- 使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
二.什么是文件
磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
1.程序文件和数据文件
程序文件:
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件:
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
2.文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含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;
注:
- FILE 的结构在不同的C编辑器中包含的内容并不是不完全相同的,但还是颇为相似的。
- 每当打开一个文件时,系统会根据文件的状况自动创建一个 FILE 结构的变量,并填充其中的信 息,只要文件被读写发生变化,文件信息区也会跟着发生变化。至于文件变化时文件信息区是怎么变化和修改的,我们其实并不需要关心这些细节,因为C语言已经帮你弄好了。
- 我们一般会通过一个 FILE 的指针来维护这个 FILE 结构的变量。并不会直接使用,而是拿一个结构体指针指向这个结构,通过这个指针来访问和维护相关的数据,这样使用起来会更加方便。
下面我们来创建一个 FILE* 的指针变量:
FILE* pf; //文件指针变量
定义 pf 是一个指向 FILE 类型的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区的信息就能够访问该文件。 也就是说,通过文件指针变量能够找到与他关联的文件。
2.fopen函数和fclose函数
头文件:stdlib.h
ANSIC 规定使用 fopen 函数来打开文件, fclose 函数来关闭文件。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
打开方式如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
用法演示:打开手动创建的 test.dat 文件
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test.dat", "w");
//检查是否为空指针
if (pf == NULL) {
perror("fopen");
return 1;
}
/* 写文件 */
fclose(pf); //关闭文件
pf = NULL; //记得将pf置为空指针
return 0;
}
运行结果:代码正常运行
现在我们把 test.dat 文件删除,然后按 r 的方式 打开文件:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test.dat", "r");
//检查是否为空指针
if (pf == NULL) {
perror("fopen");
return 1;
}
/* 写文件 */
fclose(pf); //关闭文件
pf = NULL; //记得将pf置为空指针
return 0;
}
运行结果:
四.文件的顺序读写
1.顺序读写函数一览表
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
2.主要输入输出函数介绍
(1)字符输出函数futc
int fputc(int char, FILE *stream)
将参数 char 指定的字符写入到指定的流 stream 中,并把位置标识符向前移动 (字符必须为一个无符号字符)。适用于所有输出流。
用法演示:
#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 被创建了 ,并且里面写着 abc 三个字符。
(2)字符输入函数fgetc
int fgetc(FILE *stream)
从指定的流 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;
}
运行结果:
(3)文本行输出函数 fputs
int fputs(const char *string, FILE *stream)
将字符串写入到指定的流 stream 中(不包括空字符)。适用于所有输出流。
用法演示:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test2.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
//写文件 - 按照行来写
fputs("abcdef\n", pf);
fputs("123456\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
(4)文本行输入函数 fgets
char *fgets(char *string, int n, FILE *stream)
从指定的流 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;
}
运行结果:
n 是 4,前 3 个是从文件中读取的,最后一个是 \0
打开调试窗口验证一下:
(5)格式化输出函数 fprintf
int fprintf(FILE *stream, const char *format, …………)
fprintf 用于对格式化的数据进行写文件,发送格式化输出到流 stream 中。适用于所有输出流。
用法演示:将结构体的三个数据利用 fprintf 写到 test3.txt 中:
#include <stdio.h>
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;
}
运行结果:
(6)格式化输入函数 fscanf
int fscanf(FILE *stream, const char *format, …………)
fscanf 用于对格式化的数据进行读取,从流 stream 读取格式化输入。适用于所有输入流。
用法演示:利用 fscanf 读取 test3.txt 中的内容,并打印:
#include <stdio.h>
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) );
//将读到的数据打印
printf("%s %d %f\n", p1.name, p1.dpi, p1.sens);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
(7)二进制输出函数 fwrite
size_t fread(const void* buffer, size_t size, size_t count, FILE *stream)
写一个数据到流中去,把 buffer 所指向的数组中的数据写入到给定流 stream 中。
用法演示:创建一个 test4.txt,用 fwrite 写入一个数据到 text4.txt 中去:
#include <stdio.h>
//二进制的形式写
struct S {
char arr[10];
int num;
float score;
};
int main(void) {
struct S s = { "abcde", 10, 5.5f };
FILE* pf = fopen("test4.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
//写文件
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
为什么是乱码?为什么 abcde 不是乱码?
- 我们刚才用的都是文本编译器,文本编译器打开二进制形式的文件完全是两种状态。
- 因为字符串以文本形式写进去和以二进制形式写进去是一样的,但是对于整数、浮点数等来说就不一样了,文本形式写入和二进制形式写入完全是两个概念。
(8)二进制输入函数 fread
size_t fread(void *buffer, size_t size, size_t count, FILE *stream)
从流中读取,从给定流 stream 读取数据到 buffer 所指向的数组中。
用法演示:用 fread 读取 text4.txt 中的二进制数据:
#include <stdio.h>
//二进制的形式读
struct S {
char arr[10];
int num;
float score;
};
int main(void) {
struct S s = { 0 }; //存放处
FILE* pf = fopen("test4.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;
}
运行结果:
五.文件的随机读写
1.文件指针定位函数 fseek
int fseek(FILE *stream, long offset, int origin)
根据文件指针的位置和偏移量来定位指针。
offset 是偏移量。
origin 是起始位置,有三种选项:
- SEEK_CUR 当前文件指针的位置开始偏移。
- SEEK_END 文件的末尾位置开始偏移。
- SEEK_SET 文件的起始位置开始偏移。
用法演示:创建一个文件,打开文件并随便写点内容
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
经过上一章节的介绍,这种读写方式为顺序读写,如果我们想得到 aab,该怎么做?
我们可以试着使用 fseek 函数:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, -1, SEEK_CUR); //SEEK_CUR为当前文件指针位置,偏移量为-1,向前移动1个单位
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
尝试用 SEEK_SET ,打印 a d e:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, 3, SEEK_SET); //SEEK_SET为文件的起始位置,偏移量为3,向后移动3个单位
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
尝试用 SEEK_END ,打印 a e f :
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, -2, SEEK_END); //SEEK_END为当前文件末尾位置,偏移量为-2,向前移动2个单位
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
2.返回偏移量函数 ftell
long ftell(FILE *stream)
返回文件指针相对于起始位置的偏移量。
用法演示:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
//调整文件指针
fseek(pf, 5, SEEK_CUR); // SEEK_CUR为当前文件指针位置,偏移量为5,向后移动5个单位
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
//返回偏移量
int ret = ftell(pf);
printf("%d\n", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
3.文件指针回到起始位置函数 rewind
void rewind(FILE *stream)
rewind(意为倒带,磁带倒带),设置文件位置为给定流 stream 的文件的开头,让文件指针回到起始位置。
用法演示:
#include <stdio.h>
int main(void) {
FILE* pf = fopen("test5.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
//调整文件指针
fseek(pf, 5, SEEK_CUR); //SEEK_CUR为当前文件指针位置,偏移量为5,向后移动5个单位
//返回偏移量
int loc = ftell(pf);
printf("fseek调整文件指针后:%d\n", loc);
//让文件指针回到起始位置
rewind(pf);
//再次返回偏移量,看看是不是回到起始位置了
loc = ftell(pf);
printf("使用rewind后:%d\n", loc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
由此可见,使用rewind后,文件指针返回到起始位置了
六.流 的概念
在介绍文件的顺序读写函数时,观察表格我们可以发现有的函数是适用于所有xx流的。比如fputc函数就适用于所有输出流,也就是说它不仅仅可以给文件里写。
那么什么是流?
- 我们假设写了一个程序,有时候想把数据信息打印到屏幕上,有时候想保存到硬盘里等,那么,该程序就要操作各种各样的硬件,这些硬件不同,它们的读写方式也不同,程序若想读写这些硬件就必须知到这些硬件的读写方式,这对于一个程序员来说要求和成本过高,这时候就有人提出了流的概念:
- 在程序与各硬件之间抽象出一个层,叫做流(数据像水流一样),这个流再把相应的数据写入到不同的设备中,至于这个流是怎么向外部设备读写的,程序员不必关心。
C语言默认打开3个流:
- stdin - 标准输入流 - 键盘
- stdout - 标准输出流 - 屏幕
- stderr - 标准错误流 - 屏幕
演示:我们用流向屏幕上输出信息 - stdout:
#include <stdio.h>
int main(void) {
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
return 0;
}
运行结果:abc
演示:fgetc 从标准输入流读取 - stdin
#include <stdio.h>
//使用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;
}
运行结果:
七.文本文件和二进制文件
1.文本文件
如果要求在外存上以 ASCII 码的形式存储,则需要在存储之前进行转换。以 ASCII 字符的形式存储的文件,就是文本文件。
2.二进制文件
数据在内存中以二进制的形式存储,如果不加以转换地输出到外存,就是二进制文件。
3.数据在文件中的存储方式
一个数据在文件中是如何存储的呢?
有以下两条规则:
- 字符一律以 ASCII 形式存储。
- 数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储。
代码演示: 整数10000,如果以 ASCII 码的形式形式输出到磁盘,则磁盘中占用5个字节(每个字符占1个字节)。而如果以二进制的形式输出,则在磁盘上只占4个字节:
#include <stdio.h>
int main(void) {
int a = 10000;
FILE* pf = fopen("test6.txt", "wb");
if (pf == NULL) {
perror("fopen");
return 1;
}
//写文件
fwrite(&a, sizeof(int), 1, pf); //二进制的形式写到文件中
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
我们尝试在编辑器(vs2022)中打开二进制文本文件test6.txt:
我们来检查一下 10000 是不是 10 27 00 00:
文本文件和二进制文件的存储方式
- 文本文件:将内存里的数据转换成 ASCII 码值的形式存储到文件中。
- 二进制文件:将内存里的二进制数据不加任何转化直接存储到二进制文件中。
八.文件读取结束的判定
1.经常被错误使用的“feof”函数
int feof(FILE *stream)
正确用途:在文件结束时,判断文件因为何种原因导致文件结束的函数:判断是因为读取失败而结束,还是因为遇到文件尾而结束。如果文件结束,则返回非0值,否则返回0。
错误用途:在文件读取过程中,不能用 feof 函数的返回值直接判断文件是否结束!feof 函数不是用来判断文件是否结束的函数,而是在文件已经结束时,判断是什么原因导致文件结束的。
用法演示:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int ch = 0;
FILE* pf = fopen("test.txt", "r");
if (!pf) { //pf == NULL
perror("fopen");
return EXIT_FAILURE; //符号常量EXIT_FAILURE,表示没有成功地执行一个程序
}
//fgetc - 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ( (ch = fgetc(pf)) != EOF ) {
putchar(ch);
} printf("\n");
//判断文件结束的原因
if (ferror(pf)) { // ferror - 检查是否出现错误。
puts("读取失败错误(I/O error when reading)");
} else if (feof(pf)) {
puts("遇到文件尾而结束(End of file reached successfully) ");
}
//文件关闭
fclose(pf);
pf = NULL;
}
运行结果:
2.正确判定文件是否读取结束的方法
文本文件读取是否结束,判断返回值是否为 EOF(fgetc),或者 NULL(fgets),例如:
- fgetc 函数在读取结束时会返回 EOF,正常读取时,返回读取到的字符的 ASCII 码值。
- fgets 函数在读取结束时会返回 NULL,正常读取时,返回存放字符串的空间的起始地址。
- fread 函数在读取结束时会返回 实际读取到的完整元素的个数,如果发现读取到的完整的元素个数小于指定的元素个数,那么就是最后一次读取了。
九.文件缓冲区
1.什么是文件缓冲区
ANSIC 标准采用缓冲文件系统来处理数据文件,所谓的缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块文件缓冲区。规则如下:
- 如果从内存向磁盘输出数据,会先送到内存中的缓冲区,缓冲区装满后再一起输送到磁盘上。
- 如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
- 缓冲区的大小根据C编译系统决定的。
2.冲刷缓冲区函数 fflush
int fflush(FILE *stream)
强迫将缓冲区内的数据写回参数 stream 指定的文件中。刷新成功返回 0 ,如果发生错误则返回 EOF ,且设置错误标识符,即 feof 。fflush 不适用于高版本VS
用法演示:感受文件缓冲区的存在
#include <stdio.h>
#include <windows.h>
int main(void) {
FILE* pf = fopen("test7.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;
}
运行结果如下:
此时文本文件中没有数据,是因为此时数据暂时留存在文件缓冲区里。
用 fflush 函数刷新缓冲区,此时文本文件中就有数据了。
总结:因为有缓冲区的存在,C语言在操作文件时,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
本篇到此结束,码文不易,还请多多支持哦!