对比之前C语言——通讯录管理系统初始版本,2.0版本有以下优化:
1.
采用链表实现(之前版本是顺序表实现的,导致通讯录容量有限,现在使用链表实,实现了动态开辟空间,不浪费空间,也不会出现空间不够用的情况)
2.
实现了文件的读取和保存(程序开始前会读取磁盘中已有的通讯录文件信息,程序结束后会保存内存中的通讯录信息到磁盘文件)
3.
程序界面也有一定优化
姓名
、电话
、地址
等内容(可以根据需要自行决定通讯录成员内容信息)people.h
:内定义了联系人应该包含的基本信息的结构体,还有一些和联系人结构体函数有关的声明#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
typedef struct People
{
char name[20];
char phone[14];
char address[20];
}PEO, * PPEO;
//宏定义打印格式
#define FORMAT "%-10s %-20s %-10s\n"
void printPeopleInfo(PEO people);//打印联系人信息
void inputPeopleInfo(PPEO people);//录入联系人信息
int cmpByName(PEO data1, PEO data2);//比较两个联系人的姓名是否一致,是一致返回1,不是一致返回0
SqList.h
:单链表的结构体定义和相关函数的声明#include
#include
#include
#include"people.h"
typedef struct People elemType;
typedef struct SqList
{
elemType data;
struct SqList* next;
}SqList, * pList;
pList CreatList();//创建一个带头结点的单向链表
pList BuyNode(elemType data);//创建一个节点
void HeadAdd(pList list, elemType data);//向链表中插入数据,头插法
void Delete(pList list, elemType x);//删除链表中的数据x
void Change(pList list, elemType x, elemType newdata);//修改数据
size_t Find(pList list, elemType x);//查找数据X,返回节点位序
void Destroy(pList list);//销毁链表
void Print(pList list);//打印链表中的数据
我们先从main()
函数开始,像剥洋葱似的,一层一层往下完善。
main()
函数内容十分简答,里面就一个test()
测试函数,
int main()
{
test();
return 0;
}
因为这个程序是打算把通讯录程序的主要运行代码放在test()函数中去,这是一个良好的代码习惯,把测试代码单独封装成一个函数,有很多好处:
然后我们继续完善test()
函数,test()
函数主要内容是一些测试通讯录的函数,
void test()
{
//创建一个通讯录链表
pList contact = CreatList();
//读取磁盘文件到内存(如果有文件的话)有文件读取文件内容,没有文件创建文件,所以采用‘w’方式打开文件
readFile(contact, "contact.txt");
do
{
//打印通讯录菜单界面
menu();
//读取用户输入,根据用户输入跳转到相应函数;之中包含了退出通讯录时保存数据
keydown(contact);
system("pause");
system("cls");
} while (1);
}
test()
函数是维护通讯的函数,一整个程序我们都是在维护一个单链表。所以,test()
函数首先通过单链表中的函数CreatList()
创建一个带头结点的单链表并完成该单链表的初始化,这里取名为contact
,他的类型是结构体pList
类型(结构体指针类型)。
接着,就是首先要从磁盘文件中读取数据,如果之前有存储联系人信息的话,就把联系人信息先读取到内存中,然后插入到创建好的单链表中,完成文件的读取。这里把从磁盘中读取文件内容到链表中的操作封装成一个函数 readFile
;函数的返回值为void
,函数的参数有两个,分别是单链表的头指针list
和要读取的文件名filename
,
这里还考虑了文件不存在的情况(即第一次使用通讯录时。磁盘中没有通讯录文本文件),文件不存在时,就在当前代码路径下创建一个通讯录文本文件。
void readFile(pList list, const char* filename)
{
FILE* pf = fopen(filename, "r");//以只读的形式打开文件,如果文件不存在,则打开文件失败,返回NULL指针
if (pf == NULL)//如果返回NULL说明不存在此文件,这里用“w”方式打开,就会创建一个文件
{
pf = fopen(filename, "w");
fclose(pf);
return;
}
else
{
PEO temp = { 0 };//初始化结构体
while (fscanf(pf, "%s%s%s", temp.name, temp.phone, temp.address) != EOF)//这里写%-10s%-10s%-10s会报错,写宏定义FORMAT也会报错
{
HeadAdd(list, temp);
}
fclose(pf);
}
}
读取磁盘文件结束后就是正式的进入通讯录管理系统的主要界面了↓↓↓↓↓↓
主要运行程序通过一个do-while
循环实现,显示打印选择菜单,然后是用switch
分支语句,根据用户的输入来跳转到指定操作的函数中去,为了保证界面的整洁和美观,一个操作结束后,我们先用system("pause")
提示用户输入任意键继续,接着用system("cls")
命令进行清屏处理。
然后就又是一样,打印菜单,获取用户输入,跳转到指定操作,完成操作后输入任意键,清屏-----直到用户输入0
,或者用户输入6
的时候,跳转到指定操作后,内部有exit(0)
退出程序命令,会使得程序结束运行。
代码如下:
void keydown(pList list)
{
int choose = -2;
printf("请输入你的选择(0--6):");//这里用户如果输入字符程序会陷入死循环,只能输入数字,这是一个小bug
scanf("%d", &choose);
switch (choose)
{
case 0:
printf("正常退出\n");
//保存内存中的信息到文件中
saveToFile(list, "contact.txt");
exit(0);
break;
case 1:
AddPeople(list);
break;
case 2:
ShowContact(list);
break;
case 3:
DeltePeople(list);
break;
case 4:
FindPeople(list);
break;
case 5:
ChangePeople(list);
break;
case 6:
DestroyContact(list);
exit(0);
break;
default:
printf("输入错误,请重新输入\n");
break;
}
}
代码写到这里,程序的基本架构已经有了,接下来就是补全程序的功能了,也就是把对应的函数实现了。
void saveToFile(pList list, const char* filename)
//保存内存中数据到磁盘文件中首先是saveToFile
函数,这里的情景是,如果用户输入0
的话,意思就是用户想要退出程序,这时就要把内存中的数据保存到磁盘文件中去。
这个函数有两个参数,分别是单链表的头指针和文件名称,返回值是void
,内容是首先是fopen
打开文件,然后是遍历链表,用fprintf
函数把链表中每个节点在的数据保存到文件中,最后关闭文件指针,输出提示“保存文件成功”。
代码如下:
void saveToFile(pList list, const char* filename)
{
FILE* fp = fopen(filename, "w");
SqList* pmove = list->next;
if (fp == NULL)
{
printf("文件保存失败\n");
return;
}
while (pmove != NULL)
{
fprintf(fp, FORMAT, pmove->data.name, pmove->data.phone, pmove->data.address);
pmove = pmove->next;
}
fclose(fp);
printf("文件保存成功\n");
}
AddPeople(pList list)
//添加联系人程序一开始就是基于单链表实现的,所以添加联系人就可以利用单链表的头插法函数接口,直接使用头插法,现在需要获得链表节点的数据域,就是需要获取用户输入,之前在people.h
和people.c
文件中有准备相关函数
AddPeople(pList list)
{
PEO data = { 0 };
inputPeopleInfo(&data);
HeadAdd(list, data);
}
首先我们先创建一个PEO
类型的结构体变量data
,用来存储用户输入的联系人信息,我们先初始化为0;接着调用inputPeopleInfo
函数,获取用户输入的联系人信息到变量data
中。
void inputPeopleInfo(PPEO people)
{
printf("请输入联系人的姓名:");
scanf("%s", people->name);
printf("请输入联系人的电话:");
scanf("%s", people->phone);
printf("请输入联系人的地址:");
scanf("%s", people->address);
}
之后就是熟悉的带头结点的链表的头插法了,HeadAdd
带头结点的单链表的头插法,先创建新节点,把新节点的next
指针指向原来的单链表的第一个有效节点,之后再处理头节点的next
指针的指向,让其指向新插入的节点。
//在SqList.h文件中有编辑,typedef struct People elemType;这里的elemtype就是PEO
void HeadAdd(pList list, elemType data)
{
assert(list);
pList node = BuyNode(data);
node->next = list->next;
list->next = node;
printf("Add success \n");
}
void ShowContact(pList list)
//显示所有联系人/打印通讯录直接上代码
void ShowContact(pList list)
{
if (list == NULL)
{
printf("通讯录为空,没有数据\n");
return;
}
printf(FORMAT, "姓名", "电话", "地址");
Print(list);//调用打印链表的函数接口
}
打印链表,循环遍历链表的节点,打印数据域中的内容
void Print(pList list)
{
pList pcur = list->next;
while (pcur)
{
printPeopleInfo(pcur->data);//调用打印PEO结构体数据的函数,在people.c文件中实现的
pcur = pcur->next;
}
}
void printPeopleInfo(PEO peole)
{
printf(FORMAT, peole.name, peole.phone, peole.address);
}
void DeltePeople(pList list)
//删除联系人其实关于通讯录中的添加联系人、删除联系人、修改联系人、查找联系人、清空联系人都对应单链表的插入数据、删除数据、修改数据、查找数据、销毁链表的操作,所以完全可以利用链表的函数接口,对接口进行小小的修改就好。
这里就不再赘述,直接上代码。
void DeltePeople(pList list)
{
PEO people = { 0 };
printf("请输入你要删除的联系人的姓名:");
scanf("%s", people.name);
Delete(list, people);
}
使用了链表中删除元素的函数:
void Delete(pList list, elemType x)
{
assert(list);
pList pcur = list->next;
pList prev = list;
while (pcur)
{
if (cmpByName(pcur->data,x))//比较用户输入的姓名和链表节点中的姓名是否一致,是一致返回1,不是一致返回0
{
prev->next = pcur->next;
free(pcur);
pcur = NULL;
printf("Delete success\n");
return;
}
prev = prev->next;
pcur = pcur->next;
}
printf("Delete fail\n");
}
int cmpByName(PEO data1, PEO data2)
{
if (strcmp(data1.name, data2.name) == 0)
{
return 1;
}
else
return 0;
}
void FindPeople(pList list)
//查找联系人和删除联系人的操作类似,也是用链表的查找节点函数
void FindPeople(pList list)
{
PEO people = { 0 };
printf("请输入你要查找的联系人的姓名:");
scanf("%s", people.name);
Find(list, people);
}
当时实现单链表的时候,这个函数本来是要返回节点在链表中的位序的,现在可以不用返回位序,稍稍修改↓↓↓↓↓
size_t Find(pList list, elemType x)
{
assert(list);
int count = 1;
pList pcur = list->next;
while (pcur)
{
if (cmpByName(pcur->data, x))
{
printf(FORMAT, "姓名", "电话", "地址");
printf(FORMAT, pcur->data.name, pcur->data.phone, pcur->data.address);
printf("Find success\n");
return count;
}
pcur = pcur->next;
count++;
}
printf("Find fail\n");
}
void ChangePeople(pList list)
//修改联系人信息关于修改联系人信息,这里涉及到要用户重新输入联系人信息的操作,所以这里要再调用录入联系人信息的函数inputPeopleInfo
void ChangePeople(pList list)
{
PEO people = { 0 };
printf("请输入你要修改的联系人的姓名:");
scanf("%s", people.name);
PEO newdata = { 0 };
inputPeopleInfo(&newdata);
Change(list, people, newdata);
}
void Change(pList list, elemType x, elemType newdata)
{
assert(list);
pList pcur = list->next;
while (pcur)
{
if (cmpByName(pcur->data, x))
{
pcur->data = newdata;
printf("Change success\n");
return;
}
pcur = pcur->next;
}
printf("Change fail\n");
}
void DestroyContact(pList list)
//清空联系人/销毁通讯录这里用Destroy()
销毁单链表后,但是磁盘文件中还是有之前联系人的信息,为了把磁盘中的文件中的存储的信息也销毁,这里用fopen
以"w"
(只写)的方式打开文件,这样会把原有的文件内容给覆盖掉,然后就fclose()
关闭文件,DestroyContact
结束后就exit(0)
退出程序。
至于为什么要退出程序,是因为我在测试程序的时候发现,销毁通讯录后再进行其他操作都会使得程序崩溃,这是一个BUG
,我目前没有办法解决;同时我也发现,如果用exit(0);
退出程序后,再进入程序就还能正常运行,所以,我这里设定,销毁通讯录后退出程序。
void DestroyContact(pList list)
{
Destroy(list);
printf("通讯录销毁,联系人清空\n");
//新建磁盘文件,覆盖掉原来的文件
FILE* fp = fopen("contact.txt", "w");
if (fp == NULL)
{
printf("文件保存失败\n");
return;
}
fclose(fp);
list = NULL;
}
void Destroy(pList list)
{
pList pcur = list->next;
pList next = NULL;
while (pcur)
{
next = pcur->next;
free(pcur);
pcur = next;
}
free(list);
list = NULL;
}
这个项目让我初步体会到,一些函数接口的妙用。比如说这个小项目,分成了5个文件,单链表的2个文件、描述联系人信息数据的2个文件,这些文件事先已经实现了很多函数。
之后在test.c文件中就只用调用已有的函数接口就可以了,这让代码逻辑变得很清楚,我差不多能想象一个项目多人合作的场景了,大家写的不一样的文件,但是最后都可以互相调用。实在是妙啊~