目录
1.问题定义
(1)问题背景,学校为提升同学的C语言能力开展实训项目。
(2)开发要求,实现对联系人有增加、删除、查找、修改、排序、保存、插入、统计等等
(3)开发环境,Visual Studio 2022,.c
功能图
2.系统设计
2.1总体设计
通讯录系统功能模块图
(1)通讯录系统以带头双向循环链表实现 ,由五大功能组成。
(2)输入记录模板,从键盘逐个输入联系人信息。
(3)查询记录模板,按姓名进行查找。
(4)更新记录模板,对通讯录进行维护过程中可以通过增加,插入,删除,排序,修改进行操作。
(5)统计记录模板,ascll码表对联系人的姓名由小到大进行排序。
(6)输出记录模板,联系人的信息以链表的形式储存然后打印在屏幕上。
2.2详细设计
2.2.1数据结构设计
(1)联系人信息结构体
#define NAME_MAX 20
#define TEL_MAX 20
#define SEX_MAX 20
#define ADDR_MAX 20
#define ADDR_MAX 20
#define AGE_MAX 20
typedef struct Information
{
//个人信息
char name[NAME_MAX];
char telephone[TEL_MAX];
char sex[SEX_MAX];
char address[ADDR_MAX];
char age[AGE_MAX];
}Info;
Info 结构体储存联系人的姓名,电话,地址,年龄,将做为双链表的数据区域
(2)双链表List结构体
typedef struct List
{
Info Info;
//带头双向循环
struct List * next;
struct List * prev;
}List;
Info 为联系人数据区域
next 与prev 为双链表指针区域,用储存节点地址
2.2.2主控main()函数执行流程
进入swit语句中对应的数字执行相应的函数
switch (choice)
{
case quit:
Quit(phead); //退出
break;
case add:
Add(phead); //添加
break;
case delete:
Delete(phead); //删除
break;
case find:
Find(phead); //查找
break;
case revise:
Revise(phead); //修改
break;
case show:
Show(phead); //显示
break;
case sort:
Sort(&phead); //排序
break;
case empty:
Empty(phead); //清空
break;
case position:
Position(phead); //定位
break;
case insert:
Insert(phead); //插入
break;
default:
printf("输入错误!请重新输入\n");
printf("\n");
break;
}
2.2.3输入记录模块
通过malloc函数申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,
在每块内存块区域中定义该两个内存块的的指针都指向本身内存块并且储存相应的联系人的信息,通过上面的方法再创建一个类似的内存块但是不储存类型人的信息。最后用每个内存块的指针将内存块互相连接。
2.2.4查询记录模块
通过头节点访问下一个节点中联系人的信息
将输入联系人的姓名与链表中联系人姓名用strcmp函数判断
若为零查找成功否则继续访问下一个节点联系人的信息
直到循环到头节点,若在之前没有则输出没有该联系人
具体过程看图
情景一:联系人存在
情景二:联系人不存在
2.2.5更新记录模块
对通讯录进行维护过程中可以通过增加,插入,删除,排序,修改进行操作。那么对这五个功能依此简绍
(1)增加
通过头节点定义一个结构体其名称为tail=phead->prev
然后输入添加联系人的信息其结构体为Info1
最后将两个节点依此连接
具体过程见图:
(2)插入
通过头节点访问下一个节点info1用strcmp函数判断输入的姓名与该节点的姓名是否相同
如果姓名相同,通过自定义的 Creact()函数创造一个新的节点newnode
通过info1的节点记录下一个节点info2
最后将这三个节点连接
如果不相同访问下一个info1=info1->next
如果info1==phead输出姓名错误
具体过程见图:
(3)删除
输入你所要删除的姓名
通过头节点访问下一个节点info1
用info1节点联系人中的姓名与输入姓名用strcmp函数判断
相同访问下一个节点info2
然后将phead与info2俩节点连接(具体过程见图)
不相同访问下一个info1=info1->next
直到info1==phead输出:没有删除类型人
(4)排序
通过phead->next访问下一个节点info1
然后与info1->next下一个节点info2比较
通过取出info1与·info2的姓名用strcmp函数判断
如果小于零通过自定义swap()函数传址调用将里面的信息交换
大于等于零不变
之后的节点依次判断
然后用冒泡排序
(5)修改
输入修改人的姓名
通过头节点访问第一个联系人的信息info1-phead->next
用strcmp函数判断是否相等
如果为零更改nfo1里面联系人信息
否则继续访问下一个info1=info1->next
直到info1==phead输出联系人姓名错误
2.2.6统计记录模块
通过头节点访问下一个节点info1=phead->next
打印info1里面联系人的信息
然后通过info1节点访问下一个节点info1=info1->next
依此类推
打印联系人里面的信息
2.2.7函数功能描述
(1)Init()
函数原型:List* Init()用于创造头节点
(2)Creact()
函数原型:List* Creact()用于储存输入新的联系人创造节点
(3)PushBack()
函数原型:void PushBack(List* phead)用于将新创造的节点与头节点连接
(4)Erase()
函数原型:void Erase(List* pos)用于删除pos位置结构体中联系人信息
(5)Quit()
函数原型:void Quit(List* phead)退出程序
(6)Add()
函数原型:void Add(List* phead)添加联系人
(7) Delete()
函数原型:void Delete(List* phead)与Erase()函数联用删除某一位置联系人
(8)Find()
函数原型:List* Find(List* phead)输入姓名查找某一联系人的电话,地址,年龄
(9)Revise()
函数原型:List* Revise(List* phead)输入姓名修改联系人修改信息
(10)Show()
函数原型:void Show(List* phead)显示通讯录中所有联系人的姓名
(11)Sort()
函数原型:void Sort(List** phead)以姓名ascll码值排序,由小到大
(12)swap1()
函数原型:void swap1(List* first, List* cur)c传址调用对两个节点中联系人信息交换与Sort()一起用
(13)Empty()
函数原型:void Empty(List* phead)格式化通讯录中联系人信息
(14)Position()
函数原型:void Position(List* phead)定位某一联系人在第几个位置
(15)Insert()
函数原型:void Insert(List* phead)在某一位置之后插入新的联系人
3.系统实现
3.1编码
3.1.1程序预处理
#pragma once //项目定义
#define _CRT_SECURE_NO_WARNINGS 1 //scanf授权
#define NAME_MAX 20 数组定义大小
#define TEL_MAX 20
#define SEX_MAX 20
#define ADDR_MAX 20
#define ADDR_MAX 20
#define AGE_MAX 20
#include<stdio.h> //main头文件
#include<stdlib.h> //strcmp、malloc、strcpy头文件
#include<assert.h> //assert头文件
#include<string.h> //sizeof头文件
typedef struct Information
{
//个人信息
char name[NAME_MAX]; //联系人信息
char telephone[TEL_MAX];
char sex[SEX_MAX];
char address[ADDR_MAX];
char age[AGE_MAX];
}Info; //简化结构体
typedef struct List
{
Info Info; //调用Info结构体
//带头双向循环
struct List * next; //尾指针
struct List * prev; //头指针
}List; //简化结构体
3.1.2主函数main()
void Test()
{
List* phead = Init();
int choice = 0;
enum list { quit, add, delete, find, revise, show, sort, empty, position, insert }; //枚举各个功能
do
{
Menu(); //打印菜单
printf("请选择:\t ");
scanf("%d", &choice);
choice = (enum list)choice; //选择功能
switch (choice)
{
case quit:
Quit(phead); //退出
break;
case add:
Add(phead); //添加
break;
case delete:
Delete(phead); //删除
break;
case find:
Find(phead); //查找
break;
case revise:
Revise(phead); //修改
break;
case show:
Show(phead); //显示
break;
case sort:
Sort(&phead); //排序
break;
case empty:
Empty(phead); //清空
break;
case position:
Position(phead); //定位
break;
case insert:
Insert(phead); //插入
break;
default:
printf("输入错误!请重新输入\n");
printf("\n");
break;
}
}
} while (choice); //判断是否继续
}
int main()
{
Test();
return 0;
}
3.1.3主菜单界面
用户进入通讯录系统时,需要显示主菜单,提示用户进行选择,完成相应任务。此代码被main()函数调用。
void Menu()
{
printf("\n");
printf("\t 通讯录 \n");
printf("\n");
printf("\t***********************************\n");
printf("\t****** 0.退出 1.添加 ******\n");
printf("\t****** 2.删除 3.查找 ******\n");
printf("\t****** 4.修改 5.显示 ******\n");
printf("\t****** 6.排序 7.清空 ******\n");
printf("\t****** 8.定位 9.插入 ******\n");
printf("\t***********************************\n");
}
3.1.4初始化头结点
创造一个内存块区域,作为头结点,前尾指针分别指向自己
List* Init()
{
List* phead = (List*)malloc(sizeof(List)); //创造一个内存块区域,作为头结点
assert(phead);
phead->next = phead;
phead->prev = phead;
return phead;
}
3.1.5新节点记录
给新节点赋值联系人的相关信息
前尾指针分别指向自己
List* Creact()
{
List* node = (List*)malloc(sizeof(List));
assert(node);
printf("请输入姓名:\t");
scanf("%s", node->Info.name);
printf("请输入电话:\t");
scanf("%s", node->Info.telephone);
printf("请输入性别:\t");
scanf("%s", node->Info.sex);
printf("请输入地址:\t");
scanf("%s", node->Info.address);
printf("请输入年龄:\t");
scanf("%s", node->Info.age);
printf("信息录入成功!\n");
node->next = node;
node->prev = node;
return node;
}
3.1.6尾插节点记录
将新节点(输入新联系人信息)尾插链表后面,
void PushBack(List* phead)
{
assert(phead);
List* tail = phead->prev; //保存最后一个节点地址,方便尾插
List* newnode = Creact();
tail->next = newnode; //尾插步骤,若看具体过程见上面2.2.5插入图
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
3.1.7 删除某一节点信息
首先必须保存该节点的前一个节点与后一个节点,防止链表无法连接
void Erase(List* pos)
{
assert(pos);
List* prev = pos->prev; //保存该节点的前一个节点
List* next = pos->next; //保存该节点的后一个节点
prev->next = next; //该节点的前一个节点与后一个节点连接
next->prev = prev;
free(pos); //将该节点地址制空内容销毁
pos = NULL;
}
3.1.8退出程序记录
void Quit(List* phead)
{
assert(phead); //头节点地址制空,内容销毁
free(phead);
phead = NULL;
printf("退出程序\n");
exit(0);
}
3.1.9添加联系人记录
输入需要添加联系人的个数,然后进入循环调用尾插函数,最后将各个联系人连接
void Add(List* phead)
{
int i = 0;
printf("请输入添加联系人的个数\n");
scanf("%d", &i);
for (int j = 0; j < i; ++j)
{
printf("请输入第%d位联系人的信息:\n",j+1);
PushBack(phead); //调用尾插函数,将联系人依此连接
}
printf("你所添加的%d位联系人成功录入\n", i);
}
3.1.10删除联系人记录
输入需要删除联系人的姓名
通过头节点依此访下个节点
通过strcmp函数判断是否相同
void Delete(List* phead)
{
printf("请输入你需要删除联系人的姓名!\n");
char name1[NAME_MAX] = { 0 };
scanf("%s", name1);
List* cur = phead->next;
assert(phead); //防止为空指针
assert(cur);
while (cur!=phead)
{
if (strcmp(cur->Info.name, name1) == 0) //srtcmp函数判断是否节点联系人与
//输入联系人ascll码表相同
{
Erase(cur);
printf("删除成功\n");
return;
}
cur = cur->next;
}
printf("输入错误,没有该联系人!\n");
}
3.1.11查找联系人信息
输入需要查找联系人的姓名
通过头节点访问下个节点
用strcmp函数判断是否相同
相同依此输出该节点联系人的信息
赋值通过该节点继续访问下个节点
直到访问到头节点
List* Find(List* phead)
{
char name1[NAME_MAX] = { 0 };
assert(phead);
printf("请输入所需要查找人的姓名:\n");
scanf("%s", name1);
List* cur = phead->next;
while (cur!=phead)
{
if (strcmp(cur->Info.name, name1) == 0)
{
printf("%s的电话是%s\n", name1, cur->Info.telephone);
printf("%s的地址是%s\n", name1, cur->Info.address);
printf("%s的年龄是%s\n", name1, cur->Info.age);
printf("%s的性别是%s\n", name1, cur->Info.sex);
return cur;
}
cur = cur->next;
}
printf("没有该联系人信息\n");
return ;
}
3.1.12修改联系人信息
创建修改联系人信息菜单
通过修改数字修改相关内容
用styncpy函数将该节点与需要修改的信息拷贝
List* Revise(List* phead)
{
List* newnode = Find(phead);
printf("*************************\n");
printf("***1->姓名 2->电话***\n");
printf("***3->性别 4->地址***\n");
printf("***5->年龄 0->退出***\n");
printf("*************************\n");
printf("请输入你需要修改的项目的数字\n");
int i = 0;
printf("\n");
scanf("%d", &i);
while (i)
{
if (i = 1)
{
printf("请输入新的名字!\n");
char newname[NAME_MAX] = { 0 };
scanf("%s", &newname);
strncpy(newnode->Info.name, newname, NAME_MAX);
List* newnode = Find(phead);
return ;
}
else if (i = 2)
{
printf("请输入新的电话!\n");
char newtelephone[TEL_MAX] = { 0 };
scanf("%s", &newtelephone);
strncpy(newnode->Info.telephone, newtelephone, TEL_MAX);
List* newnode = Find(phead);
return ;
}
else if (i = 3)
{
printf("请输入新的性别!\n");
char newsex[SEX_MAX] = { 0 };
scanf("%s", &newsex);
strncpy(newnode->Info.sex, newsex, SEX_MAX);
List* newnode = Find(phead);
return ;
}
else if (i = 4)
{
printf("请输入新的地址!\n");
char newaddress[ADDR_MAX] = { 0 };
scanf("%s", &newaddress);
strncpy(newnode->Info.address, newaddress, ADDR_MAX);
List* newnode = Find(phead);
return ;
}
else if (i = 5)
{
printf("请输入新的年龄!\n");
char newage[AGE_MAX] = { 0 };
scanf("%s", &newage);
strncpy(newnode->Info.age, newage, AGE_MAX);
List* newnode = Find(phead);
return ;
}
else
{
printf("输入错误,请重新输入\n");
return ;
}
}
return ;
}
3.1.13显示所有联系人信息
通过头节点访问下个节点联系人的信息然后打印,将前面一句话在循环语句执行
循环语句执行的开始条件是该节点!=头结点
void Show(List* phead)
{
List* pop = phead->next;
if (phead->next == phead)
{
printf("当前通讯录没有信息\n");
printf("\n");
return;
}
while (pop != phead)
{
printf("\n");
printf("%s的电话是%s\n", pop->Info.name, pop->Info.telephone);
printf("%s的地址是%s\n", pop->Info.name, pop->Info.address);
printf("%s的性别是%s\n", pop->Info.name, pop->Info.sex);
printf("%s的年龄是%s\n", pop->Info.name, pop->Info.age);
printf("\n");
pop = pop->next;
}
}
3.1.14名字排序记录
通过头结点访问第一个节点
通过第一个节点访问第二个节点
上面两句话在循环中写入
在循环中用strcmp函数比较姓名的ascll码值
然后以冒泡排序方法以姓名的ascll码值由小到大排序
void Sort(List** phead)
{
List* first = (* phead)->next;
List* cur = first->next;
assert(first);
assert(cur);
while (cur != (* phead))
{
for (; first != (* phead); first = first->next)
{
for (;cur!=(* phead); cur = cur->next)
{
if (strcmp(first->Info.name, cur->Info.name) < 0)
{
swap1((struct List*)&(first->Info), (struct List*)&(cur->Info));
}
}
List* curs = first->next;
cur = curs;
}
}
Show(*phead);
printf("排序完成!\n");
}
//对名字排序里面函数
void swap1(List* first, List* cur)
{
List* newnode = Init();
newnode->Info = first->Info;
first->Info = cur->Info;
cur->Info = newnode->Info;
}
3.1.15格式化记录
将头结点地址制空
创造新的头结点
void Empty(List* phead)
{
assert(phead);
free(phead->next);
phead->next = phead;
phead->prev = phead;
printf("所有联系人都格式化\n");
printf("\n");
}
3.1.16定位记录
输入需要定位联系人的姓名
在while循环中依此访问每个节点并且节点与输入姓名用strcmp比较
循环开始的条件节点!=头结点
void Position(List* phead)
{
assert(phead);
int i = 1;
List* cur = phead->next;
printf("请输入你需要定位人的姓名\n");
char name1[NAME_MAX] = { 0 };
scanf("%s", name1);
while (cur != phead)
{
if (strcmp(cur->Info.name, name1) == 0)
{
printf("%s的位置在第%d个联系人", name1, i);
return;
}
else
{
i++;
cur = cur->next;
if (cur == phead)
{
printf("没有%s联系人\n", name1);
}
}
}
}
3.1.17插入
输入需要在第几个联系人后面插入的姓名
通过循环依此访问每个节点并且通过strcmp函数判断是否相同
循环执行条件节点!=头结点
void Insert(List* phead)
{
printf("请输入需要插入的位置前一个人的姓名\n");
char name1[NAME_MAX] = { 0 };
scanf("%s", name1);
assert(phead);
List* cur = phead->next;
while (cur!=phead)
{
if (strcmp(cur->Info.name, name1) == 0)
{
List* newnode = Creact();
List* next = cur->next;
cur->next = newnode;
newnode->prev = cur;
newnode->next = next;
next->prev = newnode;
printf("插入成功\n");
Show(phead);
return;
}
else
{
cur = cur->next;
if (cur == phead)
{
printf("姓名输入错误\n");
}
}
}
}
3.2调试与测试
3.2.1调试
通过F10与F11、断点、监控进行对函数调试
3.2.2测试
主菜单选择1,然后输入添加联系人的个数,依此将联系人录入
主菜单选择2,输入想要删除联系人的姓名,然后将该联系人信息全部删除
主菜单选择5,显示现在通讯录所以联系人信息
主菜单选择4,修改某个联系人的信息
主菜单选择9,在某个联系人之后插入一个新的联系人并将所有联系人信息打印
主菜单选择8,定位某个联系人位置
主菜单选择6,以姓名的ascll码值排序
主菜单选择7,格式化联系人
主菜单选择0,退出通讯录
4.归纳总结
4.1开发经验
(1)模块化,将头文件,功能函数,主函数分开写
(2)功能化,利用系统写好的函数解决相关问题
4.2实训中遇到的问题及解决方法
(1)问题:对通讯录联系人以姓名排序,对带头双向循环链表的节点交换并不是非常熟悉运用
解决:不交换节点只交换联系人中的信息
(2)问题:在一些循环中语句一直死循环,并不是循环执行条件错误
解决:用return;放入适当的位置,结束循环
(3)问题:不知道怎么比较字符型大小
解决:用strcmp函数比较判断
(4)问题:交换联系人的信息是显示从类型不兼容提示
解决:传址调用和强制类型转换
4.3设计中尚存的不足之处
(1)对排序的冒泡排序时间复杂度较大并不适合大量联系人的排序
(2)页面不美观,不能将上一界面的信息清空
(3)对查找联系人的依此查找时间复杂度非常的大并不适合大量联系人
4.4感想和心得体会
(1)对一些函数能理解深一层的用法
(2)对带头双向循环链表能够清晰的理解
(3)加深对行参对实参改变问题发现