目录
前言:
一、文件概述:
1.为什么使用文件:
2.什么是文件:
①.程序文件:
②.数据文件:
③.文件名:
二、文件顺序读写:
1.文件的打开和关闭:
①.文件指针:
②.文件的打开与关闭:
2.文件的顺序读写:
三、文件随机读写:
1.fseek 函数:
2.ftell 函数:
3.rewind 函数:
总结:
️博客主页:✈️銮同学的干货分享基地
️欢迎关注:点赞收藏✍️留言
️系列专栏:【进阶】C语言学习
️代码仓库:VS2022_C语言仓库
家人们更新不易,你们的点赞和⭐关注⭐真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
本文重点 :
文件概述 文件顺序读写 文件随机读写
前面我们已经完成了对C语言各语法原理与使用的进阶,而这节课我们将要尝试通过学习文件操作,来提升我们的程序功能实现的能力,帮助我们更好的处理程序目标要求。废话不多说我们这就进入今天的学习吧!
前面我们在学习了结构体之后,写出了一个阶段性综合练习程序——通讯录。当我们将我们写出的通讯录运行起来后,我们可以在通讯录内完成增加、删除、修改和查询联系人信息等操作。
但是,我们此时进行的所有操作都是在计算机内存中进行的,并且我们所操作的数据也是存放在计算机内存中的,一旦我们退出了程序,内存空间就会被释放并回收,而这个过程中我们所操作的所有的数据也将不复存在。如此,我们无法将数据真正保留下来,只能在每次运行程序时重新进行输入,重新录入联系人信息,这样的通讯录使用起来极为不便。
所以,我们的目的便是希望将数据保留在本地,只有当我们进行删除操作时,才将对应的数据删除掉,即尝试实现数据的持久化。而我们实现数据持久化的方式一般有两种:使用数据库或将数据存放至本地磁盘中。
目前我们还没有接触到数据库的知识,于是我们便通过学习文件操作,来将我们的数据存放至我们计算机的本地硬盘中,从而实现数据的持久化。
我们通常所说的文件,一般是指存放在我们计算机本地硬盘上的文件。但是在我们的程序设计中,则指的是程序文件与数据文件两种文件(根据文件功能分类)。
★ 程序文件主要包括源程序文件、目标文件和可执行程序文件。
源程序文件(后缀为 .c):
目标文件( Windows 环境下后缀为 .obj):
可执行程序文件( Windows 环境下后缀为 .exe):
数据文件的内容不一定是程序,而是程序运行过程中所进行读写的数据,比如程序运行中需要从中读取的数据,或者程序运行完毕所输出的文件。
而我们今天所讨论的,正是这些数据文件的相关操作。
在前面所有内容的学习中,我们所有的输入输出,其操作对象都是终端,均为从键盘读取输入内容,并将处理结果输出致我们的计算据显示器上进行反馈。而今天我们的目的则是将数据信息输入至我们的本地磁盘上,而当我们想要对数据进行操作时,再从本地硬盘进行读取。
文件和我们人类有自己的名字籍贯一样,也需要有一个文件标识符,而为了方便起见我们常常将这个文件标识符称为文件的文件名,文件名的存在就是为了便于我们进行识别和引用。
而一个文件的文件名由三部分组成:文件路径 + 文件名主干 + 文件后缀。
以文件名“ c:\code\test.txt ”为例:
★ 其文件路径为“ c:\code\ ”,表示文件存放在硬盘 C 盘下的 code 文件夹内。
★ 其文件名主干为“ test ”,表示该文件的文件名为 test。
★ 其文件后缀为“ .txt ”,表示该文件的文件类型为文本文件。
我们在使用或操作我们的文件之前,首先需要在我们的程序中及将其打开,于是我们就来研究一下文件的打开和关闭方式。
在我们开始研究文件的打开和关闭之前,我们首先就需要了解一下文件指针这个概念,这是因为不光是文件的打开与关闭,包括后面我们在对我们的文件进行操作时,也都是通过文件指针实现的。
在我们的缓冲文件系统中,最最关键的一个概念就是“ 文件类型指针 ”,即我们通常所说的“ 文件指针 ”。并且我们要知道,我们使用的每一个文件都在内存中开辟了相应的文件信息区,用于存放该文件的相关信息,并且这些信息都保存在一个结构体变量中。
并且这样的结构体类型是有系统声明的,取名为 FILE。
例如在 Visual Studio 的头文件 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* p;
//定义一个文件指针p
像这样,我们就能创建出一个文件指针,而接下来就可以使这个文件指针 p 指向某个文件信息区(FILE 类型的结构体变量),并通过该文件信息区中所保存的信息来访问本地硬盘内的文件了。换句话说,我们通过使用文件指针就可以找到与其相关联的文件了。
我们应当在读写文件之前打开文件,并在文件读写结束后关闭文件。同时 ANSIC 规定,使用 fopen 函数(file open)来打开文件,用 fclose 函数(file open)来关闭文件。
fopen 函数的使用方式为:
FILE* p = fopen(const char* filename, const char* mod);
★ 其中“ const char* filename ”指文件名(是字符串,文件名即文件标识符)。
★ 其中“ const char* mod ”指文件打开模式(也是字符串,后面会为大家列出)。
例如:
int main()
{
//打开文件:
FILE* p = fopen("test.txt", "r");
//以"r",即只读模式打开文件c:\code\test.txt
//默认路径为.c文件同目录下
if (p == NULL)
//判断文件打开是否成功
{
perror("FILEOPEN");
//打开失败打印错误原因并退出
return 1;
}
printf("success\n");
return 0;
}
fclose 函数的使用方式为:
fopen(FILE* strname);
★ “ FILE* strname ”指的是指向期望关闭文件的文件指针。
例如:
int main()
{
//打开文件:
FILE* p = fopen("test.txt", "r");
//以"r",即只读模式打开文件c:\code\test.txt
if (p == NULL)
//判断文件打开是否成功
{
perror("FILEOPEN");
//打开失败打印错误原因并退出
return 1;
}
printf("success\n");
//验证文件是否成功打开
fclose(p);
p = NULL;
if (p == NULL)
{
printf("SUCCESS\n");
//验证文件是否成功关闭
}
return 0;
}
因为我们在打开文件时没有特意注明路径,则默认路径为 .c 文件的同目录下。为了验证我们代码的正确性,我们在该目录下创建“ test.c ”文件用于代码测试:
接下来我们将我们上面的代码编译运行查看文件打开与关闭的操作结果:
在上面的代码中,创建成功才会打印字符串“ success ”,可以得出结论文件打开成功;同时只有当文件完成关闭操作并完成指针置空后才会打印字符串“ SUCCESS ”。于是我们可以得知我们的文件打开与关闭操作成功完成。
③.文件打开模式:
文件打开模式没有要点,这里仅整理成表格供大家查阅(三个一组):
(注:优先了解前三种,二进制文件操作很少使用)
文件打开模式 | 含义 | 文件不存在 |
---|---|---|
r | 只读 -> 为了输入数据,打开已经存在的文件 | 出错 |
w | 只写 -> 为了输出数据,打开文件 | 建立新文件 |
a | 追加 -> 为了输出数据,打开文件 | 建立新文件 |
rb | 只读 -> 为了输入数据,打开二进制文件 | 出错 |
wb | 只写 -> 为了输出数据,打开二进制文件 | 建立新文件 |
ab | 追加 -> 向二进制文件尾添加数据 | 出错 |
r+ | 读写 -> 为了读和写,打开文件 | 出错 |
w+ | 读写 -> 为了读和写,建立新的文件 | 建立新文件 |
a+ | 读写 -> 打开文件,在文件尾进行读写 | 建立新文件 |
rb+ | 读写 -> 为了读和写打开二进制文件 | 出错 |
wb+ | 读写 -> 为了读和写,建立新的二进制文件 | 建立新文件 |
ab+ | 读写 -> 打开二进制文件,在文件尾进行读写 | 建立新文件 |
首先我为各位小伙伴们整理出了文件读写所使用的函数(两个一组):
函数名 | 功能 | 适用流 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入函数 | 文件 |
fwrite | 二进制输出函数 | 文件 |
这其中较为常用的就是 fputc 函数与 fgetc 函数,我们一般就通过这两个函数来实现对文件内容的顺序读写。
这两个函数的使用方式为:
fputc(const char charname, FILE* strname);
fget(FILE* strname);
我们来举例看看这两个函数的使用方式。
首先我们来看文本文件 test.txt 中的内容,即为空,没有内容:
接下来我们使用“ 写 ”模式打开该文件,并在判断非空后使用 fputc 函数来进行顺序写入:
int main()
{
FILE* p = fopen("test.txt", "w");
//文件打开模式为“写”
if (p == NULL)
{
perror("FILEOPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
free(p);
p = NULL;
return 0;
}
我们等待程序编译运行并完成数据写入后关闭程序,这时我们来到本地文件中查看硬盘中本地文件的数据写入情况:
我们可以就看到本地文件中的内容已经实现了数据的顺序写入。
完成后我们再用“ 读 ”模式打开该文件,并在判断非空后使用 fgetc 函数来顺序读取该文件中的内容:
int main()
{
FILE* p = fopen("test.txt", "r");
//文件打开模式为“读”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
int ch = 0;
while ((ch = fgetc(p)) != EOF)
{
printf("%c ", ch);
//顺序读取文件指针pp指向文件内的信息并打印
}
fclose(p);
p = NULL;
return 0;
}
然后我们将程序编译运行起来查看我们 fgetc 函数的读取结果:
我们可以清楚的看到,我们的程序实现了对本地数据的顺序写入与读取。
并且我们也可以使用 fputs 函数(区别于 fputc 函数)来实现字符串的顺序写入:
int main()
{
FILE* p = fopen("test.txt", "w");
//文件打开模式为“写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
fputs("The test TXT\n", p);
//fputc 为写入字符,fouts 为写入字符串
//只写入字符串内容,不会自动换行,想要换行需手动添加换行转义字符\n
//并且在写入时,会覆盖原本的内容数据
fputs("The test TXT", p);
fclose(p);
p = NULL;
return 0;
}
或使用 fgets 函数(区别于 fgetc 函数)来实现字符串的顺序读取:
int main()
{
FILE* p = fopen("test.txt", "r");
//文件打开模式为“读”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char arr[256] = { 0 };
//定义字符数组用于存放读取到的字符串
fgets(arr, 256, p);
//从文件指针p指向文件处,读取最多256个字符,并将数据读取至字符数组arr中
//该函数为按行读取,读取至换行转义符\n处主动停止并换行
printf("%s", arr);
//想要读取两行就需要使用两次fgets函数
fgets(arr, 256, p);
printf("%s", arr);
fclose(p);
p = NULL;
return 0;
}
而本文的最终目的,就是要结合文件操作将数据保存至本地硬盘中,从而实现对我们通讯录的使用进行优化,于是我们可以使用 fprintf 函数实现将结构体变量的内容保存至本地硬盘之中:
typedef struct Contact
{
char name[20];
char sex[5];
int age;
char tele[11];
}con;
int main()
{
FILE* p = fopen("test.txt", "w");
//文件打开模式为“写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
con c1 = { "銮同学","男",20,"123456789" };
fprintf(p, "%s %s %d %s\n", c1.name, c1.sex, c1.age, c1.tele);
//按照"%s %s %d %s\n"的格式将数据c1.name, c1.sex, c1.age, c1.tele写入至p所指向的文件内
fclose(p);
p = NULL;
return 0;
}
将程序编译运行结束后关闭,这时我们再去本地文件中查看会发现,数据已经成功的保存至本地硬盘中了:
并且我们也可以通过 fscanf 函数从本地硬盘文件中读取数据:
typedef struct Contact
{
char name[20];
char sex[5];
int age;
char tele[11];
}con;
int main()
{
FILE* p = fopen("test.txt", "r");
//文件打开模式为“读”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
con c1 = { 0 };
fscanf(p, "%s %s %d %s", c1.name, c1.sex, &(c1.age), c1.tele);
//按照"%s %s %d %s"的格式,从p所指向的文件中将数据读取至c1.name, c1.sex, &(c1.age), c1.tele中
printf("%s %s %d %s\n", c1.name, c1.sex, c1.age, c1.tele);
fclose(p);
p = NULL;
return 0;
}
在学会了顺序读写的同时,我们很多时候并不是要进行顺序读写,而是进行随机读写(伪随机,指不按照顺序依次进行读写)。为了实现这样的操作,我们就需要使用 fseek 、ftell 和 rewind 三个函数来帮助我们对这样的操作进行实现。
★ fseek 函数的作用为,根据文件指针的位置和偏移量来定位文件指针。
其使用格式为:
int fseek(FILE* strname, long int offset, int origin);
★ 其中“ offset ”为相对于指针位置的指针偏移量。
★ 其中“ origin ”为指针位置,其参数有三种:“ SEEK_CUR ”表示文件指针当前位置;“ SEEK_END ”表示文件末尾的位置;“ SEEK_SET ”表示文件开始位置。
示例:
int main()
{
FILE* p = fopen("test.txt", "r+");
//文件打开模式为“读写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fseek(p, 10, SEEK_SET);
//使用fseek函数将文件指针从文件开始处(参数SEEK_SET表示文件起始位置)指向偏移量为10处
//偏移量为正表示向后偏移,为负表示向前偏移
char output;
output = fgetc(p);
//接下来进行读取时,继续向后读取一个字符,即字符k
printf("%c\n", output);
fclose(p);
p = NULL;
return 0;
}
★ ftell 函数的作用为,返回文件指针相对于文件起始位置的偏移量。
其使用格式为;
long int ftell(FILE* strname);
示例:
int main()
{
FILE* p = fopen("test.txt", "r+");
//文件打开模式为“读写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fseek(p, -5, SEEK_END);
//使用fseek函数将文件指针从文件结尾处(参数SEEK_SET表示文件起始位置)指向偏移量为-5处
//偏移量为正表示向后偏移,为负表示向前偏移
long back = ftell(p);
//定义整型变量用于接受并记录指针相对于起始位置的偏移量
printf("指针相对于起始位置的偏移量为:%ld\n", back);
fclose(p);
p = NULL;
return 0;
}
★ rewind 函数的作用为,使文件指针位置返回文件的起始位置。
其使用格式为:
void rewind(FILE* strname);
示例:
int main()
{
FILE* p = fopen("test.txt", "r+");
//文件打开模式为“读写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fseek(p, -5, SEEK_END);
rewind(p);
//使文件指针回归文件起始位置
printf("%c\n", fgetc(p));
//打印验证指针当前位置
fclose(p);
p = NULL;
return 0;
}
经过今天的学习我们就掌握了程序对本地文件的调用与读写,就可以通过文件操作,实现对本地文件的修改与维护,同时可以对我们的通讯录程序进行再修改,使得我们的通讯录能够将数据保存在本地而不用每次打开都需要重新进行录入,使其功能更加全面,使用更加方便,帮助我们写出更加优秀、完善的程序。
既然你已经认准一条道路,何必再去打听要走多久
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!