【C语言航路】第十四站:文件
目录
一、为什么使用文件
二、什么是文件
1.程序文件
2.数据文件
3.文件名
三、文件的打开和关闭
1.文件指针
2.文件的打开和关闭
四、文件的顺序读写
1.对于输入输出的理解
2.fgetc与fputc
(1)fgetc与fputc的介绍
(2)fputc与fgetc的使用
3.fgets与fputs
(1)fgets与fputcs的介绍
(2)fputs与fgets的使用
4.fscanf和fprintf
(1)fscanf和fprintf的介绍
(2)fprintf和fscanf的使用
5.什么是所有输入所有输出流
6.fread和fwrite
(1)fread和fwrite的介绍
(2)fread和fwrite的使用
7.一些函数的对比
五、文件的随机读写
1.fseek
(1)fseek的介绍
(2)fseek的使用
2.ftell
3.rewind
六、文本文件和二进制文件
七、文件读取结束的判定
八、文件缓冲区
总结
一、为什么使用文件
在我们前面写通讯录的时候,我们发现还存在一些问题,因为里面的数据都是存储在内存中的,当我们一旦将程序关掉,数据就消失了。那么有没有什么办法能让数据不会消失呢?答案是有的。
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、
存放到数据库等方式
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化
二、什么是文件
磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
1.程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2.数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
本站讨论的是数据文件。
在以前的文章中所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件
3.文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用
文件名包含3部分:文件路径+文件名主干+文件后缀例如:D:\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结构的变量,并填充其中的信息,我们不必关心细节。
例如在vs2022下FILE是这样的,不是特别的直观
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量
#include<stdio.h>
int main()
{
//文件指针变量
FILE* pf;
return 0;
}
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件
2.文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件
下面是fopen的文档
有两个参数,一个是文件名,另外一个是打开的方式
最终返回一个FILE类型的指针
如果打开失败则返回一个空指针
下面是fclose的文档
也就是接收一个文件指针,然后关闭这个文件
如果关闭成功,返回0
如果关闭失败,返回EOF
我们用这段代码来演示一下
我们打开D:\code\test.txt以只读的方式打开
#include<stdio.h>
int main()
{
FILE* pf = fopen("D:\\code\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功\n");
fclose(pf);
pf = NULL;
return 0;
}
运行结果为没有这个文件,这是因为,我们在这个路径下是没有这个文件的
而如果我们在这里创建一个test.txt文件,运行结果为打开文件成功
我们这里要注意一点的是:我们这里写的文件名有两种写法
1.绝对路径:
与上面的代码中的写法一样,从D盘、C盘等一直往下找下去
例如:D:\code\test.txt
2.相对路径
相对路径是相对于目前这个.c文件的路径
比如说我们将test.txt文件放到.c文件的路径上,那么我们的程序中的文件名就可以直接写成test.txt了,不用一路写下去了
当然我们的相对路径有时候是需要找到上一级文件的这时候我们需要写成 ../test.txt
这里要注意的是写一个../是上一级路径,如果我们想要找到前两级路径,两次../即可
注意/前必须是两个点,如果是一个点,则代表的是本级路径
下面是文件的操作
文件使用方式 | 含义 | 如果指定文件不存在 |
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文本文件 |
"a"(追加) | 向文本文件尾添加数据 | 建立一个新的文本文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建一个新的文件 | 建立一个新的文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
"rb+"(读写) | 为了读和写,打开一个二进制文件 | 出错 |
"wb+"(读写) | 为了读和写,新建一个二进制文件 | 建立一个新的二进制文件 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读写 | 建立一个新的文件 |
比如说使用w操作。那么我们如果原本的文件里面有数据的话。我们使用w,则会清空里面的数据
以w的方式运行后
文件数据被清空了
四、文件的顺序读写
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
1.对于输入输出的理解
这是我们之前对于非文件的程序的输入输出
但是对于有文件的程序而言
2.fgetc与fputc
(1)fgetc与fputc的介绍
下面是fgetc的库函数文档
可见,fgetc的功能是将文件里面的一个字符给读取并返回
如果读成功,返回这个字符。并且将指针往后移动一位,以便读取下一个字符
如果读取错误,返回EOF。
如果位置指示符位于文件末尾,该函数返回EOF并设置流的EOF指示符(feof)。
如果发生其他读取错误,该函数也返回EOF,但设置其错误指示符(ferror)
下面是fputc的库函数文档
读取一个字符,将这个字符写入文件中
如果读取成功,返回写入的这个字符。并且由于是顺序读写。在次读写的时候会接着往后面的读写。
如果读取错误,返回EOF,并且设置错误指示器(ferror)
(2)fputc与fgetc的使用
然后我们来应用一下这两个函数
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功\n");
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fclose(pf);
pf = NULL;
return 0;
}
我们也可以使用循环的方式来读写文件
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功\n");
char ch = 0;
for (ch = 'a'; ch < 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
此时我们已经将数据全部写入了,我们来读取这些数据
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功\n");
char 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;
}
3.fgets与fputs
(1)fgets与fputcs的介绍
fputs他的作用是将一个字符串顺序读写入这个文件中,
如果成功,则返回一个非负值。
在错误时,函数返回EOF并设置错误指示符(ferror)。
fgets中
str
指向复制读取的字符串的字符数组的指针。
num要复制到str的最大字符数(包括结束的空字符)。
stream
指向标识输入流的FILE对象的指针。
Stdin可以用作从标准输入中读取的参数。
也就是说将stream这个文件指针所指向的文件里面的前num个字符复制到str数组中。这个num包含\0
返回值
如果成功,函数返回str。
如果在试图读取字符时遇到文件结束符,则设置eof指示符(feof)。如果这种情况发生在读取任何字符之前,则返回的指针是空指针(str的内容保持不变)。
如果发生读错误,则设置错误指示符(ferror)并返回空指针(但str所指向的内容可能已更改)。他是标准输入。
(2)fputs与fgets的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功\n");
fputs("hello world", pf);
fputs("hehe", pf);
fclose(pf);
pf = NULL;
return 0;
}
但是这样读入的是在一行的。如果是分行的则需要加入\n
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功\n");
char buf[20] = { 0 };
fgets(buf, 5, pf);
printf("%s\n", buf);
fgets(buf, 5, pf);
printf("%s\n", buf);
fclose(pf);
pf = NULL;
return 0;
}
还有一点需要注意的是,如果读取的过程中,遇到了\n,那就会直接停止,因为这一行已经读完了
如下面的例子中,我们的文件里面第一行就是hello world\n,就会自动换行。并且不在继续往下读取,只读取这12个字符
4.fscanf和fprintf
(1)fscanf和fprintf的介绍
下面是frintf
他的作用是,格式化的将数据写入文件中,即将内存的数据写入文件
下面是fscanf
他的作用是将pf这个文件里面的数据以格式化的形式输入到内存中
(2)fprintf和fscanf的使用
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan",18,95.5 };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fprintf(pf, "%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
5.什么是所有输入所有输出流
在我们前面的表格中,我们看到了这个东西,那么这是什么意思呢?
所谓流,就是水流的意思
对于我们的数据而言,这个数据的输出有很多种方式
比如输出到屏幕、文件、网络种
但是这些东西未免太过于繁琐
所以我们便抽象出一个流的概念。我们只需要关心将数据输出到流中即可,然后让流来将数据输送到各种输出设备中
而在我们c语言中,任何一个c语言程序运行的时候,默认打开三个流
stdin-----------标准输入(键盘) 类型:FILE*
stdout---------标准输出(屏幕) 类型:FILE*
stderr----------标准错误(屏幕) 类型:FILE*
正是由于默认打开了这三个流,所以才能让我们在输入输出的时候,不用考虑那么多
而对于文件操作而言,他不是标准输入输出错误流了。不会默认打开,所以我们就需要一开始的打开文件,来得到这个流,就可以进行输入输出数据了
而我们上面所说的所有输入流所有输出流是因为,这些函数不仅可以使用文件的流,也可以直接用标准输入输出流
6.fread和fwrite
(1)fread和fwrite的介绍
对于这两个函数,我们首先需要知道的是,这两个函数是二进制输入输出的,而上面的则是文本输入输出的,文本输入输出的我们都是可以看懂的,但是二进制的不是我们看得懂的。
其次,这个只适用于文件。不适用于其他流
对于fwrite函数而言,他有四个参数,第一个参数是要写入的数据的地址,第二个参数是一个数据的大小,第三个是数据的个数,第四个是文件
他的作用是将count个ptr处的size大小的数据,放入文件中
返回成功写入的元素总数。
如果此数字与count参数不同,则写入错误阻止函数完成。在这种情况下,将为流设置错误指示器(ferror)。
如果size或count中有一个为零,则函数返回零,错误指示符保持不变
这个参数和fwrite是一样的
但是他的作用是将文件中的数据,大小为size的count个数据放入ptr指针处
返回成功读取的元素总数。
如果这个数字与count参数不同,则要么发生了读取错误,要么在读取时到达了文件末尾
在这两种情况下,都设置了正确的指示器,可以分别使用ferror和feof进行检查。
(2)fread和fwrite的使用
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan",18,95.5 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&s, sizeof(s), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
由于是二进制的,所以我们是看不懂的
接下来我们又从文件里面将数据给读出来
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
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;
}
对于二进制的文件,我们可以使用二进制编译器来读取他
7.一些函数的对比
scanf/fscanf/sscanf
printf/fprintf/sprintf
前两组我们都已经了解了
scanf--------从键盘上读取格式化的数据 stdin
printf--------把数据写到(输出)屏幕上 stdout
fscanf--------针对所有输入流的格式化的输入函数:stdin、打开的文件
fprintf---------针对所有输出流的格式化的输出函数:stdout、打开的文件
sscanf的函数解释如下图所示
他是一个格式化输入/读取函数。将这个s字符串给格式化的读出来
sprintf如下所示
他是一个格式化输入/写入函数,将一些数据格式化的输入/写入到这个字符串中
下面是这两个函数的使用
#include<stdio.h>
struct S
{
char name[20];
int age;
float f;
};
int main()
{
struct S s = { "zhangsan",18,98.5 };
char buf[100] = { 0 };
sprintf(buf, "%s %d %f", s.name, s.age, s.f);
printf("%s\n", buf);
struct S tmp = { 0 };
sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.f));
printf("%s %d %f\n", tmp.name, tmp.age, tmp.f);
return 0;
}
所以说
sscanf--------从一个字符串中,格式化的还原一个字符串的的数据
sprintf--------把一个格式话的数据,存放到(转换成)一个字符串中
五、文件的随机读写
1.fseek
(1)fseek的介绍
这个函数有三个参数
第一个参数是文件指针
第二个参数是需要偏移的个数
第三个是相对谁而偏移的
第三个参数一般有三个取值
分别是SEEK_SET 文件的起始位置
SEEK_CUR 文件的当前位置
SEEK_END 文件的末尾位置
当我们打开文件的时候,我们会发现,开始有个光标在闪烁,这其实就是文件指针的位置
(2)fseek的使用
在下面这段代码中,我们先正常读取了abc,此时pf指向了d,但是我们使用fessk,将其调整为当前位置向左边走两个字节。就变成了b的位置。所以在次读取,结果为b
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
2.ftell
返回文件指针相对于起始位置的偏移量
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -6, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
printf("%d\n", ftell(pf));
return 0;
}
3.rewind
让文件指针的位置回到文件的起始位置
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -5, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
printf("%d\n", ftell(pf));
rewind(pf);
ch = fgetc(pf);
printf("%c", ch);
return 0;
}
六、文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节
#include<stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&a, sizeof(a), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
七、文件读取结束的判定
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数
首先文件结束了
结束后想知道结束的原因
feof 返回真,就说明文件正常读取遇到了结束标志而结束的
ferror 返回真,就说明文件在读取的过程中出错了,而结束了
比如说下面就是判断一个文本文件结束的原因
我们先打开文件,需要进行一次判断,然后当读取失败的时候会返回EOF,然后我们判断读取失败的原因,是由于I/O错误还是由于文件已经结束了导致的
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch;
while (ch = fgetc(pf) != EOF)
{
putchar(ch);
}
if (ferror(pf))
{
puts("I/O error when reading");
}
else if (feof(pf))
{
puts("End of file reached successfully");
}
fclose(pf);
pf = NULL;
return 0;
}
同样的二进制文件也是类似的,我们写完文件,在读文件的时候,读取后记录他的返回值,然后进行比较,如果相等,那就说明我们读取成功了,正常输出即可,而如果不相等,那么就说明读取出错了,这时候我们就要知道错误在哪里,如果ferror为真,是文件读取错误了,如果feof为真,那么就是文件结束了导致的
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
if (ret_code == SIZE)
{
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
}
else
{ // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp))
{
perror("Error reading test.bin");
}
}
fclose(fp);
}
八、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的
比如说:
将数据存储到硬盘,也就是文件中的时候,我们的数据是先写在缓冲区的
这时候当缓冲区满了,或者我们主动刷新缓冲区才可以将数据写入硬盘中
而主动刷新缓冲区有两种方式:
1.fflush 刷新缓冲区
2.fclose 关闭文件的时候,也会刷新缓冲区
下面这段代码,读者可以自行测试
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.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;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
总结
本小节讲解了文件的打开与关闭、顺序读写、随机读写以及文件缓冲区相关的内容
如果对你有帮助的话,不要忘记点赞加收藏哦!!!
向获得更多优质内容,一定要关注我哦!!!