环境:CLion2021.3;64位macOS Big Sur
文章目录
地表最强C语言系列传送门:
「地表最强」C语言(一)基本数据类型
「地表最强」C语言(二)变量和常量
「地表最强」C语言(三)字符串+转义字符+注释
「地表最强」C语言(四)分支语句
「地表最强」C语言(五)循环语句
「地表最强」C语言(六)函数
「地表最强」C语言(七)数组
「地表最强」C语言(八)操作符
「地表最强」C语言(九)关键字
「地表最强」C语言(十)#define定义常量和宏
「地表最强」C语言(十一)指针
「地表最强」C语言(十二)结构体、枚举和联合体
「地表最强」C语言(十三)动态内存管理,含柔性数组
「地表最强」C语言(十四)文件
「地表最强」C语言(十五)程序的环境和预处理
「地表最强」C语言(十六)一些自定义函数
「地表最强」C语言(十七)阅读程序
十四、文件
14.1 什么是文件
1.从文件的功能角度分为程序文件和数据文件:
(1)程序文件:源文件(.c)、目标文件(.obj) 、可执行程序(windows下的.exe)
(2)数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从 中读取数据的文件,或者输出内容的文件。
2.文件名:文件名包含三部分:文件路径+文件名主干+文件后缀
14.2 文件指针和流
使用文件的作用:做到数据持久化。
- 文件指针:每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件名,文件状态以及文件当前位置等),文件发生变化时文件信息区也会发生相应的变化,这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明的,取名FILE。一般使用FILE类型的指针来维护这个FILE结构体变量,也就是文件指针。不同的编译器FILE类型包含的内容不完全相同,但是大同小异。
- 流:由于存放文件的介质不同,因此想要将数据存放到文件上就需要知道目标硬件的读写规则,这无疑是很麻烦的。为了解决这一问题,在程序和硬件之间引入了流的概念,程序只需要向流中读写,由流完成对硬件的读写,而程序则不需要关心这个问题,流会处理。操作文件实际上就是操作文件流。
C语言程序只要运行起来,就默认打开了3个流:
(1)stdin - 标准输入流 - 键盘
(2)stdout - 标准输出流 - 屏幕
(3) stderr - 标准错误流 - 屏幕
这三个流都是FILE*
14.3 文件的打开和关闭
文件有以下几种打开方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
14.4 文件的顺序读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
- int fgetc( FILE *stream );
函数在读取结束的时候返回EOF,正常读取的时候,返回的是读取到的字符的ASCLL码
FILE* pf = fopen("test.txt","r");
//假设我的这个文件中有5个字符
if(pf == NULL)
{
perror("fopen");
return 1;
}
// 从文件读文件
int ret = fgetc(pf);
printf("%c",ret);
ret = fgetc(pf);
printf("%c",ret);
ret = fgetc(pf);
printf("%c",ret);
ret = fgetc(pf);
printf("%c",ret);
ret = fgetc(pf);
printf("%c",ret);
ret = fgetc(pf);//-1,已经读到文件结束了,后边的将打印乱码
printf("%c",ret);
ret = fgetc(pf);
printf("%c",ret);
fclose(pf);
pf = NULL;
- 向标准输出流中写数据
fputc('f',stdout);//向标准输出流里写入f,实际上就是在控制台打印f
fputc('u',stdout);
- 从标准输入流读取信息
int ret = fgetc(stdin);
printf("%c",ret);
- int fputs( const char* str, FILE* stream );
成功返回一个非负值,否则返回EOF
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//按照行写文件
fputs("sdada\n",pf);//换行需要体现在代码里
fputs("jlsdjgha\n",pf);
fclose(pf);
pf = NULL;
- char* fgets( char* str, int count, FILE* stream );
函数在读取结束的时候,返回NULL;
正常读取的时候,返回存放字符串的空间的起始地址
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//按照行读文件
char arr[10] = "xxxxxxxx";
fgets(arr,4,pf);//从pf中读取3个字符(因为要给\0留一个位置)存放到arr中
printf("%s\n",arr);
fgets(arr,4,pf);
printf("%s\n",arr);
fclose(pf);
pf = NULL;
- 对格式化的数据写文件
int fprintf( FILE* stream, const char* format, … );
第三个参数…实际上是可变长参数的意思,就是任意的参数个数
typedef struct S {
char arr[10];
int num;
float sc;
} S;
S s = {"fudandaxue", 20, 3.14};
FILE *ps = fopen("test.txt", "w");
if (NULL == ps) {
perror("fopen");
return 1;
}
fprintf(ps, "%s %d %lf", s.arr, s.num, s.sc);//见结构体s的数据写入ps所管理的文件test.txt
fclose(ps);
ps = NULL;
- 对格式化的数据读文件
int fscanf( FILE* stream, const char* format, … );
typedef struct S {
char arr[10];
int num;
float sc;
} S;
S s = {0};
FILE *pf = fopen("test.txt", "r");
if (NULL == pf) {
perror("fopen");
return 1;
}
fscanf(pf, "%s %d %f", s.arr, &(s.num), &(s.sc));//从test.txt中读取信息放入结构体变量s中
fprintf(stdout, "%s %d %f", s.arr, s.num, s.sc);
fclose(pf);
pf = NULL;
以上读写全部是以文本的形式而非二进制的形式读写,适用于所有输入输出流,接下来是以二进制的形式读写,只适用于文件
- size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
将buffer中count个大小为size的信息写入stream中
S s ={"asdf",20,3.14};
FILE* pf = fopen("test.txt","w");
if(NULL == pf)
{
perror("fopen");
return 1;
}
//以二进制形式写文件,除字母外,其他全部乱码,因此要读取二进制写的文件,需要使用fread,
fwrite(&s,sizeof(S),1,pf);
fclose(pf);
pf =NULL;
- size_t fread( void* buffer, size_t size, size_t count,FILE* stream );
从stream中读取count个大小为size的信息到buffer中
读取的时候,返回的是实际读取到的完整元素的个数,如果发现读取到完整的元素个数(也就是返回值)小于指定元素格式,那这是最后一次读取了。
S s[10] = {0};//用数组存方便些
FILE *pf = fopen("test.txt", "r");
if (NULL == pf) {
perror("fopen");
return 1;
}
//以二进制形式读文件,可以以正确的格式读出fwrite写的内容
fread(s, sizeof(S), 2, pf);
for (int i = 0; i < 2; ++i) {
printf("%s %d %f", s[i].arr, s[i].num, s[i].sc);
}
fclose(pf);
pf = NULL;
- 对比一组函数
scanf:针对标准输入的格式化输入语句 - stdin,即键盘
fscanf:针对所有输入流的格式化输入语句
sscanf:从一个字符串中读取一个格式化的数据
printf:针对标准输出流的格式化输出语句 - stdout,即屏幕
fprintf:针对所有输出流的格式化输出语句
sprint:把一个格式化的数据,转换成字符串
S s = {"hello", 20, 3.14f};
S tmp = {0};
char buffer[100] = {0};
//讲一个格式化数据转换为字符串
sprintf(buffer, "%s %d %f", s.arr, s.num, s.sc);
printf("%s\n", buffer);
//从buffer中还原出一个结构体数据
sscanf(buffer, "%s %d %f", tmp.arr, &tmp.num, &tmp.sc);
printf("%s %d %f\n", tmp.arr, tmp.num, tmp.sc);
14.5 文件的随机读写
int fseek( FILE *stream, long offset, int origin )
stream - 文件流
offset - 相对于初始位置的偏移量
origin - 偏移量的初始位置,有三个值: SEEK_SET, SEEK_CUR, SEEK_END,分别代表0文件头,当前位置和文件尾。
FILE *pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
int ret = fgetc(pf);
printf("%c ", ret);//a
fseek(pf, -1, SEEK_CUR);//指针从当前位置向前偏移一个字符
ret = fgetc(pf);
printf("%c ", ret);//a
ret = fgetc(pf);
printf("%c ", ret);//b
//ftell返回当前文件指针相对于起始位置的偏移量
int move = ftell(pf);
printf("%d ", move);
// rewind让文件指针回到起始位置
rewind(pf);
ret = fgetc(pf);
printf("%c ",ret);
fclose(pf);
pf = NULL;
14.6 文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加以转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCLL码的形式存储,则需要在存储前转换,以ASCLL码字符的形式存储的文件就是文本文件。
一个数据在文件中的存储方式:字符一律以ASCLL形式存储,数值型数据既可以用ASCLL形式存储,也可以使用二进制形式存储。
如整数10000,如果以ASCLL的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
int a = 10000;
FILE *pf = fopen("test.txt", "wb");
if (pf == NULL) {
perror("fopen");
return 1;
}
//以二进制形式写入
fwrite(&a, sizeof(int), 1, pf);
fclose(pf);
pf = NULL;
int res = 0;
FILE *pf2 = fopen("test.txt", "r");
if (pf2 == NULL) {
perror("fopen");
return 1;
}
//以二进制形式存的数据需要以二进制的形式读,否则会乱码,但是用文本编辑器打开文件会发现不是10000,而是一种看不懂的东西
fread(&res, sizeof(int), 1, pf2);
printf("%d", res);
fclose(pf2);
pf2 = NULL;
14.7 文件读取结束的判定
首先明确一点,文件读取结束有两种方式结束:
1.文件读完了,遇到了文件尾,这种情况我们称为正常结束;
2.读取文件过程中发生错误导致文件读取失败。
而为了判断当文件见读取结束是哪一种,很多人使用feof来判断,这种方式实际上是对feof函数的错误使用。
feof函数用于当文件读取结束时,用其返回值判断判断是读取失败结束,还是遇到文件尾结束,而不是在文件读取的过程中,判断读取文件是否结束,有关部分在C语言官网是这样介绍的:
int feof( FILE *stream )
Return value:nonzero value if the end of the stream has been reached, otherwise 0
可以看出,此函数是将函数已经读取结束作为条件,来判断是以何种方式结束。
//写代码将test.txt文件拷贝一份,生成test2.txt
FILE *pfread = fopen("test.txt", "r");
if (pfread == NULL) {
perror("pfread fopen");
return 1;
}
FILE *pfwrite = fopen("test2.txt", "w");
if (pfwrite == NULL) {
perror("pfwrite fopen");
fclose(pfread);//若pfwrite打开文件失败,需要将之前已经打开的文件关闭,防止出错
pfread = NULL;
return 1;
}
//拷贝
int ret = 0;
while ((ret = fgetc(pfread)) != EOF) {
fputc(ret, pfwrite);
}
//判断是否为正常结束
if(feof(pfread))
{
printf("遇到文件末尾,正常结束");
}
else if(ferror(pfread))
{
printf("读取文件失败,导致读取文件结束");
}
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
14.8 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
检测一下缓冲区机制是否真实存在:
FILE*pf = fopen("testBuffer.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开testBuffer.txt文件,发现文件没有内容\n");
sleep(10);//linux下的单位是秒
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘) //注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开testBuffer.txt文件,文件有内容了\n");
sleep(10);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;