上一期我们编写了一个C语言版本的简易通讯录,但是我们的之前的通讯录是没有记忆功能的,也就是说,一旦关闭了程序我们存储在里面的数据也就消失了。那么今天我们就来实现一个附带数据储存的通讯录。
在此之前,我们先来了解一下C语言中文件的读写函数:
1.fopen及fclose
fopen的作用是打开我们计算机储存的某个文件,函数返回值是FILE*类型,需要两个参数:1.文件路径 2.操作类型。下面我们来演示一下:
int main()
{
FILE* pf = fopen("data.txt", "r");
//打开文件
if (pf == NULL)
{
perror("fopen");
return -1;
}
//读文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们试着运行这段代码:
我们设置的错误函数提醒我们不存在该文件。
关于参数:
一.文件路径:我们在使用该函数的时候,打开文件所用的路径有两种:1.绝对路径 2.相对路径。
下面我们一一演示一下:
1.绝对路径:
int main()
{
FILE* pf = fopen("C:\\Users\\win10\\Desktop\\data.txt", "r");
//打开文件
if (pf == NULL)
{
perror("fopen");
return -1;
}
//读文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
要注意使用绝对路径时,原本"\"处我们应再加上一条“\”,防止其变成转义字符导致路径失效。我们运行看看:
现在我们可以看到程序运行成功没有报错。
2.相对路径:相对路径是指将文件放置在源文件的文件夹内
int main()
{
FILE* pf = fopen("data.txt", "r");
//打开文件
if (pf == NULL)
{
perror("fopen");
return -1;
}
//读文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果与绝对路径一致。
二.操作符号
下面我们尝试在文件中写入一些我们想要的数据。
int main()
{
FILE* pf = fopen("data.txt", "w");
//打开文件
if (pf == NULL)
{
perror("fopen");
return -1;
}
//写文件
fputc('h', pf);
fputc('e', pf);
fputc('l', pf);
fputc('l', pf);
fputc('o', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
可以注意到,我们使用的是相对路径,打开后的操作为写。写入数据的函数为fputs,该函数一次只能输入一个字符,第一个参数为想要输入的字符,第二个参数为先前打开文件返回的地址。下面我们来看一下运行结果:
看起来好像什么都没有发生,实际上:
打开文档的时候我们发现,我们想要的东西已经写入到了文档中。
int main()
{
FILE* pf = fopen("data.txt", "w");
//打开文件
if (pf == NULL)
{
perror("fopen");
return -1;
}
//读文件
/*fputc('h', pf);
fputc('e', pf);
fputc('l', pf);
fputc('l', pf);
fputc('o', pf);*/
fputs("hello", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
另外我们也可以使用fputs函数,一次可以输入一串字符串(如上图所示)。
实现了写入的方法后,我们不禁会去思考,既然可以把数据写入一个我们准备好的文档,那么我们是否也可以从那个文档中拿出我们想要的数据呢?答案是肯定的。下面我们就来读取我们刚刚写入到文档中的"hello"。
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
int ch = getc(pf);
printf("%c", ch);
ch = getc(pf);
printf("%c", ch);
ch = getc(pf);
printf("%c", ch);
ch = getc(pf);
printf("%c", ch);
ch = getc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
我们首先使用fopen函数打开文件,然后使用命令"r",也就是读取命令。读取字符的函数我们使用的是getc函数。该函数每次可以读取一个字符,每读取一次就会向后跳动一个字符,因此我们使用五次就可以读取我们先前储存在文档中的单词了,没读取一次我们就将它打在屏幕上:
我们也可以通过fgets函数一次性读取一行的数据:
int main()
{
char arr[20] = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
char* ch = fgets(arr,6,pf);
printf("%s",arr);
fclose(pf);
pf = NULL;
return 0;
}
它的三个参数分别是:1.读取后储存的位置 2.读取的字符(n-1个,因此在设置参数时应比想要读取的字符要多一个)3.源数据地址。
我们可以看到这个函数也很好的实现了一样的功能。
下面我们来介绍一下今天用于改造通讯录的两个函数:fread,fwrite
我们直接来看一下fwrite的用法:
struct S
{
int n;
double x;
char name[10];
};
int main()
{
struct S a = { 10,3.14,"张三" };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror(fopen);
return -1;
}
fwrite(&a, sizeof(a), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
该函数一共需要四个参数:1.数据地址 2.数据大小 3.每次写入的个数 4.写到什么地方
程序运行结果:
该函数的写入方式为二进制,所以我们未必可以直接读。
下面我们用fread来读取一下:
struct S
{
int n;
double x;
char name[10];
};
int main()
{
//struct S a = { 10,3.14,"张三" };
struct S s = { 0 };
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror(fopen);
return -1;
}
//fwrite(&a, sizeof(a), 1, pf);
fread(&s, sizeof(struct S), 1, pf);
printf("%d %lf %s",s.n, s.x, s.name);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
知道这些函数的作用和用法之后我们就可以开始对通讯录进行改造了!
首先,我们先前的通讯录的储存版本是1000个人的数据,但是在实际生活当中,因为每个人的情况不一样我们是用不了那么大的容量的。那么我们首先来实现通讯录的动态版本!
首先我们要改造的是存放数据的空间,原先我们是创造了一个结构体,在结构体内定义了一个结构体数组,数组元素个数是1000,以及一个用来记录储存人数的整形。
那么如果我们想要一个可有随储存人数变化而变化的空间,那么我们就需要以下几步:1.开辟初始空间 2.检查空间是否充足 3.充足(存入数据);不充足(开辟新空间)4.存入新数据。
那么我们首先来改造储存空间也就是通讯录结构体:
静态版本
//struct contact
//{
// struct peoinfo data [max];//100人信息存放在数组中
// int sz;//统计存放的人数
//};
动态版本
struct contact
{
struct peoinfo* data;
int sz;//统计存放的人数
int capacity;//有效容量
};
我们将原本的结构体数组改为一个结构体指针,以此来维护用以储存个人信息的空间。同时我们增加了一个整形变量capacity,它代表的是有效的容量,在后期增加人数的时候,当有效容量等于储存人数的时候我们就需要开辟新的空间用以储存新的人员信息。
当储存空间开辟方式发生变化,那么初始化函数也应当随之变化:
///静态版本
//void initcontact(struct contact* con)
//{
// con->sz = 0;
// memset(con->date,0,sizeof(struct peoinfo));
// //memset(con->date, 0, sizeof(con->date));
//}
//动态版本
void initcontact(struct contact* con)
{
con->sz = 0;
con->data = (struct peoinfo* )malloc(def * sizeof(struct peoinfo ));//开辟空间
con->capacity = def;
}
原本静态的初始化函数只是将sz,储存信息都初始化为0,在动态版本中,我们设置初始化容量为3(原本是1000),同时使用malloc函数将空间开辟为能够储存三个人信息的大小同时将这块空间的指针转化为结构体类型传递给我们刚刚设置的data指针。
到这里,之前我们提到的三步我们完成了第一步,现在我们要开始第二步:2.检查空间是否充足 3.充足(存入数据);不充足(开辟新空间)
原本的静态增加联系人的函数我们只有判断空间是否充足+新增联系人两步。而现在我们要加入第三步:空间不足时开辟新的空间。
void checksize(struct contact* con)
{
if (con->sz == con->capacity)
{
struct peoinfo* ptr = (struct peoinfo*)realloc(con->data,
(con->capacity + 2) * sizeof(struct peoinfo));
if (ptr != NULL)
{
con->data = ptr;
printf("增容成功!\n");
con->capacity += 2;
}
else
{
exit(1);
}
}//检查容量
}
首先我们封装一个判断函数,一进入该函数首先检查sz是否等于有效容量,如果容量不足我们就使用realloc函数开辟新的空间;需要注意的是,如果函数增容成功会返回一个非空指针,因此我们可以以此来判断是否增容成功,如果失败率则异常退出。
下面是完整的增加函数:
void modifycontact(struct contact* con)
{
int as = searcontact(con);
if (as >= 0)
{
printf("请输入联系人姓名:");
scanf("%s", con->data[con->sz].name);
printf("请输入联系人年龄:");
scanf("%d", &con->data[con->sz].age);
printf("请输入联系人性别:");
scanf("%s", con->data[con->sz].sex);
printf("请输入联系人电话:");
scanf("%s", con->data[con->sz].tele);
printf("请输入联系人住址:");
scanf("%s", con->data[con->sz].adr);
printf("修改成功!\n");
con->sz++;//储存人数加一
}
}
到这里我们的通讯录的空间就可以随着储存人数的增加而增加了,但是这不意味着就结束了。那些我们开辟用来存放数据的空间在程序结束以后应当重新释放掉,所以我们还有最后一步:销毁通讯录。
void destory(struct contact* con)
{
con->sz = 0;
con->capacity = 0;
free(con->data);
con->data = NULL;
}
我们用这个函数将人数和有效容量定义为0,同时释放掉我们之前开辟的空间放置内存泄漏。最后将data置为空指针。
到这里我们就将之前的通讯录成功改造成了动态内存的版本,但是这样说的通讯录依旧是不完美的,我们每次进入通讯录都需要重新录入信息才能使用,如果是这样这通讯录也就失去了它的价值,那么我们接下来将通讯录再次改造使他成为具有记忆功能。
实现记忆功能我们分为下面两大步:1.将之前输入的数据输入到我们实现准备好的文档 2.启动通讯录进行初始化后读取文档内的信息
下面我们实现第一步:将数据输入到文档中。
我们事先创建好一个TXT文档在源文件的文件夹当中,我们在结束时中加入写入函数:
case EXIT:
//储存通讯录数据
savecontact(&con);
destory(&con);
printf("退出通讯录!\n");
break;
下面是储存函数实现:
void savecontact(struct contact* con)
{
//打开文件
FILE* pf = fopen("contact.txt", "wb");
if (pf == NULL)
{
perror("savecontact::fopen");
return;//结束函数
}
//写入数据
int i = 0;
for (i = 0; i < con->sz; i++)
{
fwrite(con->data+i, sizeof(struct contact), 1, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
}
通讯录有几个人的信息我们就写入几次,该功能的实现主要依靠fwirte函数,该函数的使用我们已经在开头介绍过就不在详述了。
第二步:初始化读取信息
void initcontact(struct contact* con)
{
con->sz = 0;
con->data = (struct peoinfo* )malloc(def * sizeof(struct peoinfo ));//开辟空间
if (con->data == NULL)
{
perror(con->data);
return;
}
con->capacity = def;
//加载数据
loadcontact(con);
}
我们在最后加入加载函数,下面是加载函数的代码:
void loadcontact(struct contact* con)
{
int i = 0;
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL)
{
printf("加载失败\n");
return;
}
struct peoinfo tmp = { 0 };
while (fread(&tmp, sizeof(struct peoinfo), 1, pf))
{
checksize(con);
con->data[con->sz] = tmp;
con->sz++;
printf("加载成功!\n");
}
//关闭文件
fclose(pf);
pf = NULL;
}
需要注意的是,我们每读取一个数据前都要检查一下容量是否充足,所以我们调用了之前写的判断函数。每读取一次,我们就将信息储存到我们用来保存信息的结构体中,同时有效容量和人数都++一次。最后加载完成关闭文件。
到这里,通讯录就改造完成了哦。喜欢的话就一键三连吧!谢谢大家!