对比之前C语言——通讯录管理系统初始版本,2.0版本有以下优化:
1.
采用链表实现(之前版本是顺序表实现的,导致通讯录容量有限,现在使用链表实,实现了动态开辟空间,不浪费空间,也不会出现空间不够用的情况)
2.
实现了文件的读取和保存(程序开始前会读取磁盘中已有的通讯录文件信息,程序结束后会保存内存中的通讯录信息到磁盘文件)
3.
程序界面也有一定优化
项目效果展示
实现文件存储
一、 项目要求整理
- 多文件方式实现
- 通讯录中每个成员的信息都包含
姓名
、电话
、地址
等内容(可以根据需要自行决定通讯录成员内容信息) - 实现添加、删除、修改、查找、展示联系人信息的功能
- 将通讯录保存在文件中,若不是第一次运行程序,那么每次运行程序时会自动读取之前保存的信息
- 能对用户的错误输入进行反馈
二、代码文件整理
通讯录2.0版本这里用了5个文件来实现
people.h
:内定义了联系人应该包含的基本信息的结构体,还有一些和联系人结构体函数有关的声明
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
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<stdio.h>
#include<assert.h>
#include<stdlib.h>
#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);//打印链表中的数据
-
people.c
:对应people.h文件中函数的实现
-
SqList.c
:对应SqList.h文件中函数的实现
-
test.c
:测试文件,内含通讯录主体函数,还有main()函数
三、代码逻辑梳理
3.1 主函数main()函数
我们先从main()
函数开始,像剥洋葱似的,一层一层往下完善。
main()
函数内容十分简答,里面就一个test()
测试函数,
int main()
{
test();
return 0;
}
因为这个程序是打算把通讯录程序的主要运行代码放在test()函数中去,这是一个良好的代码习惯,把测试代码单独封装成一个函数,有很多好处:
- 代码组织和可读性:将测试代码从主要的业务逻辑中分离出来可以使代码更加清晰和易于理解。这也有助于在团队环境中工作,因为不同的开发人员可以同时处理不同的代码部分。
- 可重用性:如果测试代码是单独封装的,那么它可以轻松地在其他地方重复使用,而不需要复制和粘贴。
- 模块化:将代码分解为较小的、独立的函数或模块可以使代码更容易维护和更新。这也符合“单一职责原则”,即每个函数或类应该只做一件事情。
- 测试的独立性:如果测试代码和主要代码混在一起,那么测试可能会依赖于某些特定的实现细节,这可能会导致测试的可靠性和稳定性降低。将测试代码封装在一个独立的函数或模块中可以确保测试的独立性。
- 易于调试和错误排查:如果所有的测试代码都在一个单独的函数或模块中,那么你可以很容易地找到并修复任何问题。
- 遵循最佳实践:许多现代编程语言和开发最佳实践都推荐将测试代码与主要代码分开。
3.2 完善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;
}
}
3.3 补全keydown()函数
代码写到这里,程序的基本架构已经有了,接下来就是补全程序的功能了,也就是把对应的函数实现了。
① 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文件中就只用调用已有的函数接口就可以了,这让代码逻辑变得很清楚,我差不多能想象一个项目多人合作的场景了,大家写的不一样的文件,但是最后都可以互相调用。实在是妙啊~