这篇博客将应用C语言实现通讯录管理系统
通讯录要求如下:
通讯录能够存放联系人的复杂信息,包括:姓名、年龄、性别、电话、地址。
通讯录能够提供如下功能:
1、存放1000人信息
2、增加新的联系人信息
3、删除指定联系人信息
4、查找指定联系人信息
5、修改指定联系人信息
6、重新为通讯录排序
7、打印通讯录列表
8、指定位置插入新联系人信息
如果要存放一个人的复杂信息,包括姓名(name),年龄(age),性别(sex),电话(phone),地址(address)等,单独单独创建变量,或者数组都不好处理,所以首选结构体,我将它命名为People。
//符号常量声明
//优点:便于管理
#define NAME_MAX 20
#define SEX 5
#define PHONE 12
#define ADDRESS 20
//定义联系人结构体并将其重命名为People
//优点:增强代码可读性;减少书写工作量
typedef struct People
{
char name[NAME_MAX];
int age;
char sex[SEX];
char phone[PHONE];
char address[ADDRESS];
}People;
#define MAX 1000
//定义通讯录结构体,重命名为Contact
typedef struct Contact
{
People data[MAX];//声明联系人数组,形成一个能够存放1000信息的通讯录列表
int sz; //变量sz是用来记录列表中已经存放了几个联系人的信息
}Contact;
为什么要嵌套使用结构体?
当实现比如说增加、删除、打印的功能时,需要一个变量记录联系人数量变动,相较于额外创建一个变量,这种写法能够减少函数传参时参数的个数
在这里很明显传通讯录的地址过去比较好,理由:
1.造成严重的空间浪费。传变量过去函数会再次开辟一个与通讯录大小相同的内存空间,而通讯录所占内存空间的大小为(People结构体的大小 * 1000)
2.我们要能够访问、修改通讯录的内容
代码如下:
//初始化结构体
void IntContact(Contact* con)
{
assert(con);//断言指针,提高程序的安全性,在地址为空时强行中止程序
memset(con->data, 0, sizeof(con->data));//初始化结构体数组
con->sz = 0;//信息位置也要初始化,每当信息数量加一,sz+1.
}
功能介绍:
1.、能够判断通讯录是否满员
2、未满员前能够添加通讯录下成员
该功能位于主框架第一条分支
代码如下:
void AddContact(Contact* con)
{
assert(con);
if (con->sz == MAX)//当通讯录容量已达上限
{
printf("您的通讯录列表已满员,无法添加\n");
return;
}
printf("请输入姓名:>");
(void)scanf("%s", con->data[con->sz].name);//date是一个数组,不要忘记加[]
printf("请输入年龄:>");
(void)scanf("%d", &con->data[con->sz].age);//字符数组名本身是一个地址,但age不是,要加&
printf("请输入性别:>");
(void)scanf("%s", con->data[con->sz].sex);
printf("请输入电话:>");
(void)scanf("%s", con->data[con->sz].phone);
printf("请输入地址:>");
(void)scanf("%s", con->data[con->sz].address);
printf("本次输入成功\n");
con->sz++;//记录信息个数
}
该功能位于之框架第六条分支
先写出这个是因为打印能够帮助我们更直观地理解、修改
代码如下:
//打印通讯录
void PrintContact(Contact* con)
{
assert(con);
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n", "姓名", "年龄", "性别", "电话号码", "地址");
//\t 的意思是 :水平制表符。将当前位置移到下一个tab位置。
//%10s - 表示输出的宽度为10,其他数字同理
//负号 - 表示从默认的右对齐改为打印数据左对齐
for (i = 0; i < con->sz; i++)
{
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",
con->data[i].name,
con->data[i].age,
con->data[i].sex,
con->data[i].phone,
con->data[i].address);
要打印的联系人信息不止一个 -> 循环
}
在实现查找之前,我们来思考一个问题:查找的标准是什么?
通讯录成员的信息有五项,我们似乎有很多标准,但是实际上以年龄、性别、地址为标准查找除的联系人信息是不靠谱的,剩下的只有姓名和电话号码了。
问题来了,你说,如果我还记得电话号码,那我为啥要翻通讯录?
因此,我将姓名定义为查找标准
代码如下:
//查找联系人信息
void SearchContact(Contact* con)
{
assert(con);
char name[NAME_MAX] = { 0 };
printf("请输入要查找的联系人的名字:>");
(void)scanf("%s", name);
int pos = 0; //用变量pos记录查找到的联系人的在数组中的下标
int i = 0;
for (i = 0; i < con->sz; i++)
{
if (strcmp(name, con->data[i].name) == 0)
{
pos = i;
}
}
if (pos == -1)
{
printf("您要修改的人的信息不存在\n");
return;
}
else
{
printf("您要查找的联系人信息如下\n");
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n",
"姓名", "年龄", "性别", "电话号码", "地址");
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",
con->data[pos].name,
con->data[pos].age,
con->data[pos].sex,
con->data[pos].phone,
con->data[pos].address);
}
}
//查找联系人的姓名
static int FindName(char name[], Contact* con)
{
assert(con);
int i = 0;
for (i = 0; i < con->sz; i++)
{
if (strcmp(name, con->data[i].name) == 0)
{
return i;
}
}//有个小缺陷,如果联系人重名了咋办 - 虽然一般来说正常人的通讯录基本不会有同名的
return -1;//表示找不到
}
//查找联系人信息
void SearchContact(Contact* con)
{
assert(con);
char name[NAME_MAX] = { 0 };
printf("请输入要查找的联系人的名字:>");
(void)scanf("%s", name);
int pos = FindName(name, con);
if (pos == -1)
{
printf("您要修改的人的信息不存在\n");
return;
}
else
{
printf("您要查找的联系人信息如下\n");
printf("%-10s\t%-5s\t%-5s\t%-12s\t%-20s\n", "姓名", "年龄", "性别", "电话号码", "地址");
printf("%-10s\t%-5d\t%-5s\t%-12s\t%-20s\n",
con->data[pos].name,
con->data[pos].age,
con->data[pos].sex,
con->data[pos].phone,
con->data[pos].address);
}
}
修改,说白了就是为指定位之的结构体重新赋值
但是修改的前提是,我没们要修改的联系人信息存在,所以要加入判断语句
代码如下:
//修改联系人信息
void ModifyContact(Contact* con)
{
assert(con);
char name[NAME_MAX] = { 0 };
printf("请输入要修改的联系人的名字:>");
(void)scanf("%s", name);
int pos = FindName(name, con);
if (pos == -1)
{
printf("您要修改的人的信息不存在\n");
return;
}
else
{
printf("已成功为您找到该联系人\n");
printf("请输入姓名:>");
(void)scanf("%s", con->data[pos].name);
printf("请输入年龄:>");
(void)scanf("%d", &con->data[pos].age);
printf("请输入性别:>");
(void)scanf("%s", con->data[pos].sex);
printf("请输入电话:>");
(void)scanf("%s", con->data[pos].phone);
printf("请输入地址:>");
(void)scanf("%s", con->data[pos].address);
printf("本次修改成功\n");
}
}
功能介绍:
输入要删除的联系人的名字,判断该联系人是否存在,存在的话,执行删除
原理就是后面的元素向前覆盖
代码如下:
void DeleteContact(Contact* con)
{
assert(con);
char name[NAME_MAX] = { 0 };
if (con->sz == 0)
{
printf("您的通讯录列表为空,无需删除\n");
return ;
}
printf("请输入您要删除的联系人姓名:>");
(void)scanf("%s", name);
int pos = FindName(name, con);
//printf("%d\n", pos);
if (pos == -1)
{
printf("您要删除的人不存在\n");
return ;
}
else
{
int i = 0;
for (i = pos; i < con->sz-1; i++)
{
con->date[i] = con->data[i + 1];
//想相同类型的结构体能够直接用赋值号
}
}
//举例:
//如果pos = 3,con->sz = 5,循环走两次,第五个位置的信息没有被空白覆盖
//但其实也不需要被覆盖,当增加新的联系人时会覆盖掉
con->sz--;
printf("删除成功\n");
}
排序首先要定一个排序的标准,这里我提供了两种标准:年龄,姓名。
为了更好地实现排序,我采用库函数qsort(快排)
代码如下:
//void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
//以姓名为基准进行排序
int SortByName(const void* e1, const void* e2)
{
assert(e1 && e2);
return strcmp(((People*)e1)->name, ((People*)e2)->name);
}
//以年龄为基准进行排序
int SortByAge(const void* e1, const void* e2)
{
assert(e1 && e2);
return ((People*)e1)->age - ((People*)e2)->age;
}
//重新排序通讯录
void SortContact(Contact* con)
{
assert(con);
//printf("测试\n");
if (con->sz == 0)
{
printf("您的通讯录列表为空,无需排序\n");
return;
}
int input = 0;
printf("请问您要选择哪种顺序来排序通讯录\n");
printf("输入 1 表示以姓名为标准\n");
printf("输入 2 表示以年龄为标准\n");
printf("输入 0 表示不排序\n");
printf("请问您的选择是:>");
(void)scanf("%d", &input);
switch (input)
{
case 1:
//以姓名为基准进行排序
qsort(con->data, con->sz, sizeof(con->data[0]), SortByName);
break;
case 2:
//以年龄为基准进行排序
qsort(con->data, con->sz, sizeof(con->data[0]), SortByAge);
break;
case 0:
printf("退出排序功能\n");
break;
default:
printf("输入错误请,重新输入:>");
break;
}
printf("排序成功\n");
}
插入联系人之前判断通讯录是否满员,有空闲空间就执行插入操作
插入简单来说就是在数组中的某个地方腾出一个位置来放我们想放的信息
后面的元素都往后挪动为插入信息腾出一个空间,最后通讯录列表中的联系人数量比原来+1,与删除相比是一个相反的操作
代码如下
void InsertContact(Contact* con)
{
assert(con);
//判断当前通讯录联系人信息数量是否达到上限
if (con->sz == MAX)
{
printf("您的通讯录列表已满,无法插入\n");
return;
}
int input1 = 0, input2 = 0;
printf("请问您要在哪个位置进行信息插入:>");
(void)scanf("%d", &input1);
printf("请问您要插入几个联系人的信息:>");
(void)scanf("%d", &input2);
//在执行插入操作之前要对信息进行移动
int j = 0;
for (j = con->sz-1; j >= input1-1; j--)
{
con->data[j + input2] = con->data[j];
con->sz += input2;
}
int i = 0;
for (i = input1-1; i < input1-1 + input2; i++)
{
//执行插入操作
printf("请输入联系人信息\n");
printf("请输入姓名:>");
(void)scanf("%s", con->data[i].name);
printf("请输入年龄:>");
(void)scanf("%d", &con->data[i].age);
printf("请输入性别:>");
(void)scanf("%s", con->data[i].sex);
printf("请输入电话:>");
(void)scanf("%s", con->data[i].phone);
printf("请输入地址:>");
(void)scanf("%s", con->data[i].address);
printf("本次修改成功\n");
}
}
至此,作为零部件的各功能的函数已经实现好了
各功能的函数已经实现好了,又怎么少得了主心骨main函数呢,第一种方法,我采取比较常见的多重分支switch - case
代码如下:
//包含自定义头文件
#include "Contact.h"
//打印菜单
void menu(void)
{
printf("==================================\n");
printf("===========1.增加联系人==========*\n");
printf("===========2.删除联系人==========*\n");
printf("===========3.查找联系人==========*\n");
printf("===========4.修改联系人==========*\n");
printf("===========5.排序联系人==========*\n");
printf("===========6.打印联系人==========*\n");
printf("===========7.插入联系人==========*\n");
printf("===========0.退出程序=============\n");
printf("==================================\n");
}
//定义枚举常量
enum Choose
{
EXIT1, //默认值从0开始
ADD, //1
DELETE, //2
SEARCH, //3
MODIFY, //4
SORT, //5
PRINT, //6
INSERT //7
};
int main(void)
{
//定义变量存放指令
int input = 0;
do
{
menu();//打印菜单
printf("请选择您要进行的操作:>");
(void)scanf("%d", &input);
switch (input)
{
case ADD:
//增加联系人
break;
case DELETE:
//删除联系人
break;
case SEARCH:
//查找联系人信息
break;
case MODIFY:
//修改联系人信息
break;
case SORT:
//重新排序通讯录
break;
case PRINT:
//打印联系人信息
break;
case INSERT:
//在通讯录的某个位置插入某个一个联系人的信息
break;
case EXIT:
printf("退出程序\n");
break;
default:
printf("输入错误,请您重新输入!\n");
break;
}
} while (input);//注意点:do - while后面要加分号
return 0;
}
根据输入的 input 的值的不同,程序会执行不同的分支,从而实现不同的功能。
枚举的作用在于定义常量,来替代 case 后面的数字,从而增加代码的可读性。
但是,随着通讯录功能的增加,case分支将会越来越多,代码将越来越长,越来越累赘,那么有什么简化的方法吗?
答案是,肯定的。
我们来观察一下各个函数的声明
//增加联系人
void AddContact(Contact* con);
//打印通讯录
void PrintContact(Contact* con);
//删除联系人
void DeleteContact(Contact* con);
//修改联系人信息
void ModifyContact(Contact* con);
//查找联系人信息
void SearchContact(Contact* con);
//重新排序通讯录
void SortContact(Contact* con);
//指定位置插入联系人信息
void InsertContact(Contact* con);
显然,除了函数名不一样外,函数的参数甚至返回类型都是一样的,这样恰好符合构成函数指针数组的条件
void (*pContact[8])(Contact * con)
经过优化后的代码如下:
int main(void)
{
//定义变量存放指令
int input = 0;
//通讯录变量
Contact con;
//初始化通讯录
IntContact(&con);
do
{
menu();//打印菜单
printf("请选择您要进行的操作:>");
(void)scanf("%d", &input);
void (*pContact[8])(Contact * con) =
{ NULL, AddContact,DeleteContact,
SearchContact, ModifyContact, SortContact,
PrintContact, InsertContact
};
//注意点:如果要符合1-7的指令,则要8个元素,函数指针数组首元素存放空指针
if (input >= 1 && input <= 7)
{
pContact[input](&con);
}
else if (input == 0)
{
printf("退出程序\n");
}
else
{
printf("您的指令输入错误,请重新输入!\n");
}
} while (input);//注意点:do - while后面要加分号
return 0;
}
看,这样写是不是感觉既简洁,又高大上起来了
直接用一个数组来作为通讯录不是不行,但我们仔细想一下,一个1000人的通讯录我们真的能够一下子用完吗,显然是不能的,这样子就会造成一大片空间一直不被使用,对提高内存利用率来说毫无帮助
1.直接使用结构体数组已经不合适,由于malloc返回的是一个指针,所以用一个指针变量来管理
2.变量 sz 记录目前通讯录中联系人的信息数量
3.变量 capacity 记录目前通讯录的最大容量,在通讯录扩容后,该变量也要更新
代码如下:
//结构体Contact存放通讯录 - 动态版本
typedef struct Contact
{
People* date;
int sz;
int capacity;
}Contact;
代码如下:
#define DEFAULT 3//通讯录默认大小
#define INCREASE 2//每次扩容增加的大小
//初始化结构体 - 动态版本
void IntContact(Contact* con)
{
assert(con);
con->data = (People*)malloc(DEFAULT * sizeof(People));
//判断,空间开辟是否成功
if (con->data == NULL)
{
printf("空间开辟失败\n");
return;
}
con->sz = 0;
con->capacity = DEFAULT;
}
改动地方不大,在增加到上限时,自动扩容通讯录
代码如下:
增加联系人
void AddContact(Contact* con)
{
assert(con);
if (con->sz == con->capacity)//当通讯录容量已达上限,考虑扩容
{
printf("通讯录成员已满,程序将为你扩容\n");
people* str = (People*)realloc(con->data, (con->capacity + INCREASE) * sizeof(People));
//判断是否成功开辟新的空间
if (str == NULL)
{
printf("扩容失败\n");
return;
}
else
{
con->data = str; //将新空间的地址交给date管理
con->capacity += INCREASE;//提高上限
Sleep(700);//
printf("扩容成功\n");
}
}
printf("请输入姓名:>");
(void)scanf("%s", con->data[con->sz].name);//date是一个数组,不要忘记加[]
printf("请输入年龄:>");
(void)scanf("%d", &con->data[con->sz].age);//字符数组名本身是一个地址,但age不是,要加&
printf("请输入性别:>");
(void)scanf("%s", con->data[con->sz].sex);
printf("请输入电话:>");
(void)scanf("%s", con->data[con->sz].phone);
printf("请输入地址:>");
(void)scanf("%s", con->data[con->sz].address);
printf("本次输入成功\n");
con->sz++;//记录信息个数
}
改动与动态版添加功能类似
代码如下:
void InsertContact(Contact* con)
{
assert(con);
if (con->sz == con->capacity)//当通讯录容量已达上限,考虑扩容
{
printf("通讯录成员已满,程序将为你扩容\n");
people* str = NULL;
str = (People*)realloc(con->data, (con->capacity + INCREASE) * sizeof(People));
//判断是否成功开辟新的空间
if (str == NULL)
{
printf("扩容失败\n");
return;
}
else
{
con->data = str; //将新空间的地址交给date管理
con->capacity += INCREASE;//提高上限
Sleep(700); //暂缓0.7秒打印扩容成功
printf("扩容成功\n");
}
}
int input1 = 0, input2 = 0;
printf("请问您要在哪个位置进行信息插入:>");
(void)scanf("%d", &input1);
printf("请问您要插入几个联系人的信息:>");
(void)scanf("%d", &input2);
//在执行插入操作之前要对信息进行移动(数据备份)
int j = 0;
for (j = con->sz-1; j >= input1-1; j--)
{
con->data[j + input2] = con->date[j];
}
con->sz += input2;
//开始插入
int i = 0;
for (i = input1-1; i < input1-1 + input2; i++)
{
//执行插入操作
printf("请输入联系人信息\n");
printf("请输入姓名:>");
(void)scanf("%s", con->data[i].name);
printf("请输入年龄:>");
(void)scanf("%d", &con->data[i].age);
printf("请输入性别:>");
(void)scanf("%s", con->data[i].sex);
printf("请输入电话:>");
(void)scanf("%s", con->data[i].phone);
printf("请输入地址:>");
(void)scanf("%s", con->data[i].address);
printf("本次修改成功\n");
}
}
对于动态申请的空间,有两种释放的方法:
1、手动释放
2、关闭程序时自动释放
但是,手动释放动态申请的内存空间任然是一个好习惯,这样子可以避免产生许多bug
代码如下:
//释放通讯录空间 - 动态版本
void DestoryContact(Contact* con)
{
free(con->data);
con->data = NULL;
con->sz = 0;
con->capacity = 0;
}
虽然看起来这个通讯录已经很完善了,但其实还存在很多缺点
比如说:
1、当我们扩容到100个联系人时,确只用了20个空间,浪费了80个,这时我们自动释放一部分的空间来提高空间利用率。
2、以上内容都是以顺序表的形式存储的,但是我们也能够应用链表的的形式
3、还有查找功能,怎么实现只输入一个字,却要输出含有该字的联系人的信息
…………
问题还用很多,本人的知识储备不断完善时,将会逐渐完善它。