动态存删通讯录(数组存储)
简单介绍
这是一个适合练手的C语言小项目,使用数组存储信息,主要包含结构体的定义与应用,函数的调用,指针的传递,文件的操作,简单排序算法的应用,实现的主要功能是对通讯录信息的增删改查,还有根据年龄进行排序和保存录入信息到文件中,方便下次打开继续查看,同时通讯录的存储空间是可以动态开辟的,可以根据信息量的多少来实现实时扩容。
个人认为,这个小项目适合在学习完C语言基础语法之后就可以练习实现,加深对于各类知识的认识和运用。
使用纯C语言编写,VS2015编译运行。
运行效果
还是采用的win32命令行窗口进行编写的,没有图形化界面,但是各个功能都是可以正常使用的。
由于编码方式的问题,建议使用英文输入,如果要使用中文,可以修改项目属性中的字符编码方式实现,这里使用英文来演示!
我提前录入了四个成员的信息,在编译运行的时候就会提示,如图:
可以先查看一下我之前存入的信息:
这里我再添加一个信息:
在录入信息的时候,这里是有简单的输入检查的,下面再演示一下查找、修改和删除信息:
查找:
修改:
删除:
下面是排序和保存:
排序:这里我设想的是采用两种排序方式进行,但现在只完善了一种按照年龄排序(这个较为容易实现),采用的是冒泡排序进行的
保存:保存可以使用7.save来保存,同时在0.退出的时候,也会自动保存,这里是保存到程序创建的一个文本文档中,方便下次继续编辑通讯录信息
好了,这就是这个小项目的全部功能了,下面我们来一步一步的实现它,完成全部功能!
功能分析设想
框架设想
- 我们需要一个简单的功能选择控制台,这里可以通过一个构造一个菜单函数来实现,然后通过不同的选择,进入不同的功能,很容易的联想到,这个可以通过switch-case语句来实现
- 信息存储,由于是要存储通讯录的各个成员的姓名,年龄,联系方法等信息,我们可以先用一个结构体来存储一个成员的各个信息,再用另外一个结构体来存储各个成员及成员个数或者其他信息。
- 模块化设计,我们可以构造很多函数来实现各个功能,这样可以方便我们的维护和查找错误,主要的函数应该包括初始化,添加,删除,查找,显示,修改,排序,保存,退出等。
总体布局
由于考虑到代码量可能会比较大,可以采用添加不同的源文件来实现不同的功能,在这个项目中,我选择使用三个源文件来实现所有的功能,如图:
contact.c用于存放功能实现的主页面
contact.h用于存放所有的头文件、结构体、函数等声明
test.c用于存放所有的函数主体
我在这里就需要引用自己创建的头文件,是用引号来表示,而不是使用系统自带的头文件引用方式,引用格式如下:
#include "contact.h"
好了前期的设想工作就基本完成了,下面就可以具体实现了
代码具体实现
首先,我们需要一个结构体来存储各个通讯录成员的信息(PeoInfo),它需要包括姓名,年龄,性别,联系方式,地址等。而这仅仅是一个成员需要的信息,现在我们需要若干个成员,我们就可以再使用一个结构体(Contact),它需要包括成员的具体信息,也就是上面的那个结构体,还有就是成员的个数,如果要是到此为止,我们需要的信息似乎就已经完美实现了,但是,我们需要的是一个***动态的通讯录***,也就是说:我们要实现可以动态的分配存储空间,在刚开始时只分配很少的空间,当空间快要用完时,再次申请空间,那么我们就需要一个量来判断我们什么时候需要重新申请空间,所以我们就可以在这个结构体中再加一个成员作为记录剩余可存储空间的量,这样就可以较为完美的实现我们所需要的功能了,下面是结构体的定义:
#define MAX_NAME 20
#define MAX_SEX 5
#define MAX_TELE 12
#define MAX_ADDR 30
#define DEFAULT_SZ 3 //这个是初始化时可以存储的通讯录成员的个数
typedef struct PeoInfo
{
char name[MAX_NAME];
int age;
char sex[MAX_SEX];
char tele[MAX_TELE];
char addr[MAX_ADDR];
}PeoInfo;
typedef struct Contact //通讯录类型
{
//创建连续空间来存储信息,如果不够则重新申请,每次两个大小
struct PeoInfo *data;
int size;//记录当前已经保存的通讯录个数
int capacity; //当前通讯录的最大容量,如果超出则申请新的空间
}Contact;
结构体在定义完成之后,我们需要及时的进行初始化,下面是初始化函数:
初始化
void InitContact(struct Contact* ps) //初始化通讯录结构体
{
ps->data = (struct PeoInfo* )malloc(DEFAULT_SZ * sizeof(struct PeoInfo));
//对data的初始化,最初只有三个位置,为了修改方便,这里改用自定义的DEFAULT_SZ
if (ps->data == NULL)
{
return;
}
ps->size = 0;
ps->capacity = DEFAULT_SZ; //初始大小
//在实现将通讯录存入文件之后就可以在下次初始化的时候继续读入这些信息
LoadContact(ps); //ps 就是地址变量,所以这里不用取地址符&
}
这个函数的最下面的LoadContact函数的功能就是要实现通讯录信息的存储和再次读取,这里先不做讲解,详细的实现方法,我会放在最后解释。
初始化完成之后,我们就可以真正的考虑实现我们所需要的功能了,在这之前,我们可以先在操作台上打印出我们要实现的功能的一个菜单,每个菜单对应一个case入口,在这里,我为了提高代码的可读性,我选择使用一个枚举类型来列举我需要的功能,这样我们在后续实现每个功能时就可以更加方便了,下面是打印的菜单和各个函数的入口,详细的解释在代码注释中:
enum Option
{//与菜单操作对应
EXIT, //0
ADD, //1
DEL, //2
SEARCH, //3
MODIFY, //4
SHOW, //5
SORT, //6
SAVE //7
};
void menu(void)
{
printf("\n");
printf("***********************************\n");
printf("***** 1. add 2. delet *****\n");
printf("***** 3.search 4. modify *****\n");
printf("***** 5. show 6. sort *****\n");
printf("***** 7.save 0.exit *****\n");
printf("***********************************\n");
printf("\n");
}
void main(void)
{
int input=0;
//在声明中定义结构体,要包含所有通讯录信息
struct Contact con; //新建通讯录
//初始化通讯录
InitContact(&con);
//存入信息
do
{
menu();
printf("请选择:");
scanf("%d", &input);
/*在这里不同数字的含义不够明确,即代码可读性不高,可以使用 枚举 类型来定义改善*/
switch (input)
{
default:printf("选择错误!\n");
printf("\n");
break;
case EXIT:
SaveContact(&con); //在销毁前先保存文件
DestroyContact(&con); //销毁通讯录,释放内存
printf("退出通讯录!\n");
printf("\n");
break;
case ADD:
//存入信息
AddContact(&con);
break;
case DEL:DelContact(&con);
break;
case SEARCH:SearchContact(&con);
break;
case MODIFY:ModifyContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case SORT:SortContact(&con);
break;
case SAVE:SaveContact(&con);
break;
}
} while (input); //可以很有效的检查输入信息
}
这里要注意的一点就是,我们在test.c源文件中放置了main函数,那么在其他的源文件中就不可能再出现main函数了,不能违背一个程序只有一个main函数的原则。
在这个main函数中,我已经详细的列出了我们要实现的所有函数,下面我们就只需要依次实现这些函数就可以了
添加新成员
在添加通讯录成员的时候,我们需要考虑两个问题,是否可以添加新成员和怎么添加新成员
- 是否可以添加新成员:我们需要判断现在的结构体是否已经满了,因为我们在初始化的时候仅仅定义了3个成员变量的空间,如果已经满了,我们就需要再申请新的存储空间,那也就是说,我们需要一个函数,用来判断满并申请新的存储空间,下面是这个函数的具体实现:
void CheckCapacity(struct Contact* ps)
{
if (ps->size == ps->capacity)
{
//增容
struct PeoInfo* ptr = realloc(ps->data, (ps->capacity + 2)*sizeof(struct PeoInfo));
//在原来的地址上,再申请两个存储空间
if (ptr != NULL)
{
ps->data = ptr;
ps->capacity += 2;
printf("增容成功!\n");
}
else
{
printf("增容失败!\n");
}
}
}
这里申请新的空间使用的是realloc函数,其作用于malloc函数类似,主要区别是可以在原有的空间的基础上再申请新的空间,关于这里空间的申请及使用,我会在后续的博客中继续总结解释的。
- 如何添加新成员
下面是增加成员信息的函数,详细的代码解释在注释中:
void AddContact(struct Contact * ps) //添加通讯录信息
{
//增加容量,如果通讯录已满,则增加空间(两个),如果没满,则不做操作
CheckCapacity(ps);
printf("请输入名字:");
scanf("%s", ps->data[ps->size].name);
//存储在data区域的第size个位置的name区域
printf("请输入年龄:");
scanf("%d", &(ps->data[ps->size].age));
//因为这里传递的是地址,要将数据存储在这个位置的变量中
while (0>ps->data[ps->size].age || 150<ps->data[ps->size].age )
{
printf("年龄输入有误,请重新输入:");
scanf("%d", &(ps->data[ps->size].age));
}
printf("请输入性别:");
scanf("%s", ps->data[ps->size].sex);
printf("请输入电话号码:");
scanf("%s", ps->data[ps->size].tele);
while (11 != strlen(ps->data[ps->size].tele))//对电话号码长度的检查
{
printf("电话号码输入位数有误(11位电话号码),请重新输入:");
scanf("%s", ps->data[ps->size].tele);
}
printf("请输入住址:");
scanf("%s", ps->data[ps->size].addr);
ps->size++;
printf("添加信息成功!\n");
printf("\n");
}
在添加新信息的时候,可以包含简单的输入检查,这里我对年龄和电话号码进行了简单的输入检查,采用while循环,直到输入符合检查条件的信息才可以输入成功,当然,对于其他信息也可以添加输入检查,原理都是类似的,可以自己尝试实现。
展示成员信息
在录入通讯录信息之后,我们就可以选择把我们录入的信息进行展示了,在这个函数中,我们之间显示录入的信息,为了较为彻底的保留录入的信息不会被修改出错,可以选择使用 const 来修饰传递的通讯录成员,下面是实现代码:
void ShowContact(const struct Contact * ps)
{//传递的是地址,但是要避免改变已经存储的值的可能,所以就用const来修饰
if (ps->size == 0)
{
printf("当前通讯录中没有成员!\n");
printf("\n");
}
else
{
int i;
printf("%-20s \t %-4s \t %-5s \t %-12s \t %-20s\n", "姓名", "年龄", "性别", "电话", "地址");
//先打印一行表头
for (i = 0; i < ps->size; i++)
{
printf("%-20s \t %-4d \t %-5s \t %-12s \t %-20s\n",
ps->data[i].name,
ps->data[i].age,
ps->data[i].sex,
ps->data[i].tele,
ps->data[i].addr);
}
}
}
修改信息
在看到录入的信息后,如果发现有录入错误,我们就需要对其进行修改,关于修改,我采用的是覆盖修改,也就是直接找到要修改的记录,然后将其修改成正确的信息,再这里我们就需要有两步要进行:
- 查找要修改的信息:那这里就需要一个查找函数,这里我使用的是使用名字来进行查找信息的,这个实现很好理解,只要遍历通讯录信息,然后验证是否有我们要修改的信息就可以了,这个函数我使用了** static** 关键字进行了修饰,它起到的主要作用是把这个函数变为静态函数,
普通函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,
不能被其他文件所用。因此定义静态函数有以下好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突。
<2> 静态函数不能被其他文件所用。
下面是详细代码:
static int FindName(const struct Contact* ps, char name[MAX_NAME])
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (0 == strcmp(ps->data[i].name, name))
{
return i; //找到,返回所在的位置
}
}
return -1; //找不到该元素
}
这仅仅是一个满足查找信息的功能,我们其他函数就要利用这个函数的返回值来实现其他的功能。
当然了,查找信息不仅仅会在要删除的时候使用,在我们要查找某个通讯录成员信息的时候也会有使用,所以最好把各个功能都包装成单独的函数使用。关于在实现查找时使用我会在下面解释。
- 修改信息,下面时详细的代码实现:
void ModifyContact(struct Contact * ps)
{
char name[MAX_NAME];
printf("请输入要修改的联系人的名字:");
scanf("%s", name);
int pos = FindName(ps, name);
if (-1 == pos)
{
printf("该通讯录中没有%s!\n", name);
}
else
{
printf("请输入名字:");
scanf("%s", ps->data[pos].name);
printf("请输入年龄:");
scanf("%d", &(ps->data[pos].age));
printf("请输入性别:");
scanf("%s", ps->data[pos].sex);
printf("请输入电话号码:");
scanf("%s", ps->data[pos].tele);
printf("请输入住址:");
scanf("%s", ps->data[pos].addr);
printf("修改信息完成!\n");
printf("\n");
}
}
查找信息
在上面我们有个查找的封装的函数,我们正好可以利用这个函数来实现展示信息的功能,由上面查找函数返回的所在位置,我们就可以显示出这条信息,下面是详细的实现函数:
void SearchContact(const struct Contact * ps)
{
char name[MAX_NAME];
printf("请输入要查找的人的姓名:");
scanf("%s", name);
// 在这里的代码在 删除,修改的时候都会用到,所以这里可以封装成函数使用,使用 FindNmae函数
int pos=FindName(ps,name); //返回查找值所在的下标位置,找不到返回-1
if (-1 == pos)
{
printf("该通讯录中没有%s!\n", name);
}
else
{
printf("查找成功!\n");
printf("%-20s \t %-4d \t %-5s \t %-12s \t %-20s\n",
ps->data[pos].name,
ps->data[pos].age,
ps->data[pos].sex,
ps->data[pos].tele,
ps->data[pos].addr);
}
}
删除信息
在删除信息的时候,我们同样也要用到查找的功能,找到要删除的信息所在的位置,在找到之后就可以进行删除操作了,在这个函数中,我使用直接进行字符串匹配查找,直接找到要删除的信息所在的位置,然后进行覆盖,就是将后面的信息依次向前移动一格,覆盖掉要删除的信息记录就可以了,关于字符串匹配,我使用的是字符串函数:
int strcmp(const char* stri1,const char* str2);
它的两个参数分别是两个字符串,根据 ASCII 编码依次比较 str1 和 str2 的每一个字符,直到出现找不到的字符,或者到达字符串末尾(遇见\0)。通过函数的返回值来判断字符串比较结果。
返回值 = 0, 则表示 str1 等于 str2
返回值 < 0,则表示 str1 小于 str2。
返回值 > 0,则表示 str2 小于 str1。返回值 = 0,则表示 str1 等于 str2。
下面是详细的实现代码:
void DelContact(struct Contact * ps)
{
printf("请输入要删除的人的名字:");
char name[MAX_NAME];
scanf("%s", name);
//先查找,再删除,要找出要删除的元素所在的位置
int i = 0;
for (i = 0; i < ps->size; i++)
{
/*strcmp 字符串比较函数
C语言 strcmp() 函数用于对两个字符串进行比较(区分大小写)。头文件:string.h
语法/原型:
int strcmp(const char* stri1,const char* str2);
参数 str1 和 str2 是参与比较的两个字符串。
strcmp() 会根据 ASCII 编码依次比较 str1 和 str2 的每一个字符,直到出现找不到的字符,
或者到达字符串末尾(遇见\0)。
返回值 = 0, 则表示 str1 等于 str2
返回值 < 0,则表示 str1 小于 str2。
返回值 > 0,则表示 str2 小于 str1。返回值 = 0,则表示 str1 等于 str2。
*/
if (0 == strcmp(ps->data[i].name,name))
{
break; //此时 i 即为要删除的元素
}
}
if (i == ps->size)
{
printf("抱歉,要删除的元素不存在!");
printf("\n");
}
else
{
int j=0;
for (j = i; j < ps->size-1; j++)//数组下标要减少一个
{//从i开始,依次让后面的元素 覆盖前一个元素
ps->data[j] = ps->data[j + 1];
}
ps->size--;
printf("删除通讯录成员成功!\n");
printf("\n");
}
}
排序信息
我在这里使用的是冒泡排序进行排序实现的,当然了,可以采用其他效率更高的排序算法,这里就只是用这个函数进行演示,关于冒泡排序及其优化算法,我在之前的博客中有写过,可以参考:冒泡排序及其优化算法
在这里就不再多赘述了,关于排序,我本来设想的是采用两种排序方式,现在只写完了采用年龄进行升序排序,关于第二个采用名字首字母进行排序的我会在后续完成代码后再贴出。
下面是详细代码:
void SortContact(struct Contact * ps)
{
printf("\n");
printf("***********************************\n");
printf("***** 1. 按照年龄排序 ************\n");
printf("***** 2.按照姓名排序 *************\n");
printf("***********************************\n");
printf("请选择排序方式:");
int input;
scanf("%d", &input);
switch (input)
{
case 1:
printf("下面是排序结果:");
printf("\n");
int i, j,issort;
PeoInfo t;
for (i = 0; i <ps->size - 1; i++)
{
issort = 1;
for (j = 0; j < ps->size - i - 1; j++)
{
if (ps->data[j].age > ps->data[j + 1].age)
{
t = ps->data[j];
ps->data[j] = ps->data[j + 1];
ps->data[j+1] = t;
issort = 0;
}
}
if (issort)
break;
}
ShowContact(ps);
break;
case 2:
printf("按照姓名排序仅适用于英文名字!\n");
printf("该功能尚待完善!");
printf("\n");
break;
default:printf("输入错误!");
printf("\n");
break;
}
}
存储退出
我们要存储之前写入的通讯录信息,就要再本地使用一个文件来存储,同时在存储成功之后,我们还要在下次打开时可以实现读取这个文件,取得我们上次存储的信息,同时在写完保持函数之后,我们还要考虑到我们在菜单的退出时也要进行一次保存设置,下面是详细的代码:在注释中有详细的步骤解释,
void SaveContact(Contact* ps)
{
FILE* pfWrite = fopen("contact.txt", "wb");
//以只写模式打开(新建)一个txt文件,二进制文件
if (pfWrite == NULL)
{
printf("SaveContact: %s\n", strerror(errno));
//strerror(errno)该函数返回一个指向错误字符串的指针,该错误字符串描述了错误 errnum。
//需要头文件 #include <string.h>和#include <errno.h>
return; //无返回值的结束函数
}
//写入通讯录中数据到文件中
int i = 0;
for (i = 0; i < ps->size; i++)
{
fwrite(&(ps->data[i]), sizeof(PeoInfo), 1, pfWrite);
//写入地址,写入大小,每次写入个数,文件流
}
printf("信息已保存至contact.txt文件中!\n");
fclose(pfWrite); //写完之后要关闭文件
pfWrite = NULL;
}
在存储完信息到本地之后,我们就可以销毁内存中的通讯录了,下面是代码:
void DestroyContact(Contact* ps)
{
free(ps->data);
ps->data = NULL;
}
我们还有一点要实现,就是要在下次打开的时候,可以自动读取上次已经保存的文件,这个在上面没有详细说明,在这里详细解释下:
void LoadContact(Contact* ps)
{
FILE* pfRead = fopen("contact.txt","rb");
PeoInfo tmp = { 0 }; //临时变量来存放信息
if (pfRead == NULL)
{
printf("coadContact: %s\n", strerror(errno));//这样可以显示不同的错误信息
return;
}
//读取文件。存放到通讯录中
while (fread(&tmp, sizeof(PeoInfo), 1, pfRead))
{
//分别是 接受数据的地址,读入的一个单元的大小,每次读入的个数,文件流
//返回真实读到的元素的个数
CheckCapacity(ps); //检查容量是否可以存储,如果不可以就扩容
ps->data[ps->size] = tmp;
ps->size++; //加载容量
}
printf("成功读取上次保存的信息!\n");
fclose(pfRead);
pfRead = NULL;
}
好了,截至到现在,动态通讯录的所有框架就已经搭建完毕了,现在就可以调试运行一下了,如果没有意外的话,就可以实现我最上面的效果了。
完整代码
由于博客这里输入文字有限,我选择把全部的源代码和详细注释放在GitHub上了,下面是地址链接:源代码地址
(* ̄3 ̄)╭
最后
感谢观赏,一起提高,慢慢变强。