前言
之前编写通讯录程序时,由于数据都是保存在内存上,每次关闭通讯录后,都需要重新再输入信息。有没有办法将数据持久化的保存在硬盘中呢?这就涉及到文件操作的知识。本文将介绍文件操作相关函数的使用,并对之前写过的通讯录程序进行修改。
文件分类
从文件功能的角度来分类:
1. 程序文件
比如在用vs写代码时创建的源文件(.c),目标文件(windows环境后缀为.obj),执行文件(.exe)
2. 数据文件
比如程序运行需要从中读取数据的文件,或者需要写入数据进去的文件。
此前我所写的所有程序都是以终端为数据文件,如printf是在有需要时将数据打印在屏幕上,scanf是将键盘上的输入数据读取进程序文件中。现在不过是将终端变化为数据文件,这样就好理解多了。
文件名
文件名是唯一的文件标识,有了文件名才能准确找到文件的位置并进行操作。
文件名包含三部分:文件路径+文件名主干+文件后缀
比如:D:\Study\新建文本文档.txt
文件指针
进行文件操作时,我们需要一个桥梁沟通程序和我们希望进行操作的数据文件。这个桥梁就是文件指针FILE*
。
每个被使用的文件都会在内存中开辟一段文件信息区域,用于存放文件的相关信息,如文件名,文件的状态,当前的位置等。而FILE是编译器提供的一个结构体类型,文件信息区的内容被保存在FLIE类型的变量里。
FILE* pf;//文件指针类型变量
文件操作函数
1. 文件的打开和关闭
文件打开,使用函数fopen,关闭使用fclose。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
当fopen失败时,会返回空指针。因此最好加上判空。
其中,const char * mode
是指输入对文件操作的不同指令字符,即“打开方式”。
打开方式汇总如下表:
比如我要打开文件,并读取文件中数据,则:
FILE* pf = fopen("test.txt","r");//用只读方式打开test.txt文件
FILE* pf = fopen("D:\\Study\test.txt","r");//当使用文件路径进行访问时,需要在盘符后多加一个'\'进行转义
2. 文件的顺序读写
关于文件操作,我们可以使用以下函数:
![](https://i-blog.csdnimg.cn/blog_migrate/28dc6586cadd589e68cfdcd2a1baa61e.png)
顾名思义,fgetc和fputc是向文件输出\从文件中读取字符的函数,c == char。当成功时,fgetc和fputc会返回其得到\写入字符的ASC码。并将文件的位置标识符后移一位。
同理,fgets和fputs中,s == string。
表中函数各功能与返回值均有所不同,在此不一一介绍。
2.1 两组输入/输出函数的对比
为了更深入的理解各个函数的功能,接下来对scanf/fscanf/sscanf;printf/fprintf/sprintf这两组函数的异同之处进行比较测试。
其中,scanf、printf和fscanf、fprintf的区别在于,后两者引入了流。
以scanf和fscanf为例,查看函数定义可知:
scanf函数是从标准输入流——即键盘上读取数据的。
fscanf(stdin,"%d",&a);
和 scanf("%d",&a);
的效果相同,stdin就是标准输入流的意思。
如此一来fscanf的功能就很明确了——从一个输入流中读取数据到目标变量中。
用例如下:
先创建一个文件test.txt。将其内容编辑为15并保存退出。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test()
{
int a = 0;
//打开文件
FILE* pf = fopen("D:\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//使用
fscanf(pf, "%d", &a);//将文件中信息读取进变量a中
printf("%d", a);
//关闭文件
fclose(pf);
pf = NULL;
}
int main() {
test();
return 0;
}
运行后我们就会惊喜的发现,屏幕上输出了15。
![](https://i-blog.csdnimg.cn/blog_migrate/720530fe782709956f6df349943ca719.png)
同理,fprintf的功能即为将a中的数据打印进目标文件中。用例如下:
先将test.txt中数据清除,再运行以下代码:
void test()
{
int a = 15;
//打开文件
FILE* pf = fopen("D:\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return;
}
//使用
fprintf(pf, "%d", a);
//关闭
fclose(pf);
pf = NULL;
}
int main() {
test();
return 0;
}
运行后打开文件,发现文件内容已被修改为15。
而sscanf和sprintf两个函数与上述的区别则在于,这两个函数并非从流中读取\写入数据,而是从字符串中读取\写入格式化的数据。
用例如下:
struct s
{
char name[10];
int age;
double score;
};
void test()
{
char arr[20] = { 0 };
struct s s1 = { "zhang",20,90.1 };
//使用sprintf,将结构体内容转化为字符串并存入字符数组。
sprintf(arr, "%s %d %lf", s1.name, s1.age, s1.score);
//打印arr内容看是否成功存入
printf("%s", arr);
//使用sscanf,从字符串中读取格式化数据
struct s s2 = { 0 };
sscanf(arr, "%s %d %lf", s2.name, &(s2.age), &(s2.score));
//打印s2看是否成功读取
printf("%s %d %lf", s2.name, s2.age, s2.score);
}
int main()
{
test();
return 0;
}
运行程序后得:
说明从字符串读取\写入的功能均测试成功。
3. 文件的随机读写
3.1 fseek()
此处介绍文件随机读写所用到的函数:fseek
查看函数定义可知,fseek是用来改变文件指针位置的函数。其第一个参数为文件指针,第二个参数为偏移量,第三个参数表示开始添加偏移 offset 的位置,可选择三个值:
- SEEK_SET 文件的开头
- SEEK_CUR 文件指针的当前位置
- SEEK_END 文件的末尾
当fseek()成功时,返回0,否则返回非零值。
比如,我们可以如此使用fseek:
//文件随机读写
void test()
{
FILE* pf = fopen("D:\\test.txt", "r+");//注意此处需要使用读写,r+和 w+都可以。
if (pf == NULL)
{
perror("fopen");
return;
}
fputs("Hello, Mike!", pf);
fseek(pf, -5, SEEK_END);//从文件的末尾向前偏移5个字符
fputs("Jill!", pf);
fseek(pf, 0, SEEK_SET);
//这里也可使用rewind(pf),将文件指针位置回到文件起始位。
char arr[50] = { 0 };
fread(arr, 1, 12, pf);//还有一个'\0'
printf("%s\n", arr);
fclose(pf);
pf = NULL;
}
int main() {
test();
return 0;
}
运行结果为:
测试成功。
3.2 ftell()
ftell()函数功能为:告诉我们指定流的位置标识符当前位置是多少。
![](https://i-blog.csdnimg.cn/blog_migrate/37f8d658586007f5e6f33dc450f238ea.png)
void test()
{
FILE* pf = fopen("D:\\test.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return;
}
fputs("abcde", pf);
fseek(pf, -3, SEEK_END);//从文件的末尾向前偏移3个字符
long int pos = ftell(pf);
printf("%ld", pos);
fclose(pf);
pf = NULL;
}
int main() {
test();
return 0;
}
运行结果如下:
3.3 ferror()、feof() - 文件读取结束的判定
ferror()函数的功能,是通过其返回值判断,是否因文件读取时出错导致的文件读取结束。
feof()函数的功能,是通过其返回值来判断,是不是读取到文件末尾导致的结束。
ferror()的返回值:
feof()的返回值:
用例:
void test()
{
//事先在文件中存入abcde
FILE* pf = fopen("D:\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
int c = 0;
while ((c = fgetc(pf)) != EOF)//通过判断fgetc的返回值是否为EOF,确定文件是否读取结束
{
//putchar(c);//打印在屏幕上
printf("%c", c);
}
printf("\n");
//文件读取结束后的判定
if (ferror(pf))
{
printf("pf流有错。\n");
}
if (feof(pf))
{
printf("成功到达文件末尾。\n");
}
fclose(pf);
pf = NULL;
}
运行结果为:
对通讯录程序的修改
通过对文件操作的学习,现将通讯录程序修改为使用文件进行存储的版本。
此版本修改建立在之前发布在博客C/C++ 通讯录程序构思与实现中所展示的V1版本通讯录程序的基础上。
添加了两个函数功能:
save_contact(contact* pc)
:当在主界面选择0退出通讯录时,调用本函数,会将本次通讯录存储的内容写入到文件contact.dat中。load_contact(contact* pc)
:在程序开始时,创建通讯录结构体后,对通讯录结构体进行初始化,初始化后调用本函数,加载存储在contact.dat中的数据。
void load_contact(contact* pc)
{
FILE* pf = fopen("contact.dat", "r");
if (pf == NULL)
{
perror("load_contact::fopen");
return;
}
while (fread(pc->contact + pc->sz, sizeof(peoinfo), 1, pf))
{
pc->sz++;
}
fclose(pf);
pf = NULL;
}
//初始化函数
void init_contact(contact* pc)
{
pc->sz = 0;
memset(pc->contact, 0, Con_MAX * sizeof(peoinfo));//初始化为0
//读取通讯录
load_contact(pc);
}
//保存通讯录到文件
void save_contact(contact* pc)
{
FILE* pf = fopen("contact.dat", "w");
if (pf == NULL)
{
perror("save_contact::fopen");
return;
}
//写文件
int i = 0;
for (i = 0; i < pc->sz; i++)
{
fwrite(pc->contact + i, sizeof(peoinfo), 1, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
}
经测试,第一次在通讯录程序中存储2个人信息后,关闭通讯录。再次打开时直接选择进行打印,得到结果如下:
测试成功。
总结
本文总结了我对文件管理相关知识的学习,以做记录。