通讯录项目虽然比较简单,但是其包含的知识面比较广,大家可以当作一次阶段性学习测验,检测一下自己对于C语言基础掌握得怎么样,当然若是接触C语言没有多久的小伙伴也可以复制一下代码,尝试阅读一下,提升自己兴趣,当然我也不是大佬,同样是学者一位,这个项目同样是我对我自己能力检验。
通信录要求:
存放好友信息:名字、电话、性别、住址、年龄。
有8个功能分别为
0. 退出通信录
1. 增加信息
2. 删除信息
3. 查找信息
4. 修改信息
5. 显示信息
6. 整理信息
7. 保存信息
首先,咱们对于一个项目实现不能将功能写在一个文件当中,分文件操作相当有必要,我这次项目就分为启动文件,功能文件,以及声明所用的头文件。
头文件讲解:
创建一个结构体struct PeoInfo用于存放个人信息,在创建另一个结构体struct Cont_Book作为我们的通讯录,该结构体运用了动态内存开辟相关的知识。
使用动态内存原因:我们不希望通讯录创建的大小被定死,需要一个能够灵活存储的空间,既不浪费空间,也不会因为空间不足而存放不了数据。
后面的枚举只是为了让之后的选着语句让我们在开发时更方便书写,没有太多实际意义。
最后则是函数声明,想要跨文件使用函数必备内容。
#ifndef __Contact_H__
#define __Contact_H__
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>
#define MAX_NAME 20 //名字
#define MAX_SEX 6 //性别
#define MAX_TELE 12 //电话
#define MAX_ADDR 20 //地址
#define DEFAULT_SZ 3 //通讯录初始长度
//个人信息
struct PeoInfo
{
char name[MAX_NAME];
int age;
char sex[MAX_SEX];
char tele[MAX_TELE];
char addr[MAX_ADDR];
};
//通讯录
struct Cont_Book
{
int size; //当前已经拥有的数量
int capacity; //当前通讯录的最大容量
struct PeoInfo * data; //动态内存开辟空间
};
//枚举选择
//0,1,2,3,4,5,6,7
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT,
SAVE
};
//功能函数声明
void Init_Cont(struct Cont_Book* ps); //初始化
void Add_Contact(struct Cont_Book* ps); //增加
void Show_Contact(const struct Cont_Book* ps); //显示
void Del_Contact(struct Cont_Book* ps); //删除
void Search_Contact(const struct Cont_Book* ps); //查找
void Modify_Contact(struct Cont_Book* ps); //修改
void Sort_Contact(struct Cont_Book* ps); //排序
void Destroy_Contact(struct Cont_Book* ps); //销毁
void Save_Contact(struct Cont_Book* ps); //保存
void Load_Contact(struct Cont_Book* ps); //读取
#endif
启动文件讲解:
整个项目通过输入input选择功能,也利用input决定循环的退出条件。
因为我们希望程序不管怎样都能够执行一次,所以使用do while循环,在循环体之前需要提前创建好通讯录结构体变量con方便之后函数使用,然后需要对整个通讯录的内容初始化操作,也就是为我们的通讯录建立起始环境,初始化会在之后讲解。
在循环体内部搭建一个选择语句分别对应我们需要实现的功能。其选择的ADD、DEL等就是我之前在头文件当中建立的枚举内容,你们也可以直接写为数字。
#define _CRT_SECURE_NO_WARNINGS 1
#include"Contact.h"
//启动文件
//显示界面
void menu()
{
printf("##################################\n");
printf("##### 1. Add 2. del #####\n");
printf("##### 3. Search 4. Modify #####\n");
printf("##### 5. Show 6. Sort #####\n");
printf("##### 7. Save 0. Exit #####\n");
printf("##################################\n");
}
int main()
{
int input = 0;
//创建通讯录
struct Cont_Book con; //包含data指针,size,capacity
//初始化通讯录
Init_Cont(&con);
do
{
//显示界面
menu();
printf("请选择功能》");
scanf("%d", &input);
//功能
switch (input)
{
case ADD://增加
Add_Contact(&con);
break;
case DEL://删除
Del_Contact(&con);
break;
case SEARCH://查找
Search_Contact(&con);
break;
case MODIFY://更改
Modify_Contact(&con);
break;
case SHOW://显示
Show_Contact(&con);
break;
case SORT://排序
Sort_Contact(&con);
break;
case SAVE://保存
Save_Contact(&con);
break;
case EXIT://退出
Save_Contact(&con);
Destroy_Contact(&con);
printf("退出程序\n");
break;
default://输入错误
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
功能函数讲解:
(1)初始化函数
在启动文件当中我们调用了初始化函数,而初始化的功能表示我们需要对通讯录开辟空间,以及对起始计数值赋值。
进入函数之后需要开辟动态内存,所以我们需要利用calloc()函数,其中DEFAULT_SZ宏定义为3,表示初始创建的内存大小能够装下3个人的信息。注意对动态内存指针需要有保护,即判断开辟空间是否正确。
然后将计数值size赋值为0,将空间容量标记记为3。
Load_Contact(ps)其为文件操作,表示读取并写入进通讯录已经存在的通讯录信息,后面我会单独讲解。
//声明
void Load_Contact(struct Cont_Book* ps);
//初始化
void Init_Cont(struct Cont_Book* ps)
{
//开辟动态内存
ps->data = (struct PeoInfo*)calloc(DEFAULT_SZ, sizeof(struct PeoInfo));
if (ps->data == NULL)
{
return;
}
ps->size = 0;
ps->capacity = DEFAULT_SZ;
//加载文件信息
Load_Contact(ps);
}
(2)检测通讯录的容量大小
在进行增加操作之前需要判断已经开辟的动态内存空间是否足够,如果不够则扩容,足够不做任何操作退出。
扩容动态内存空间需要利用realloc()函数,传入参数为被扩容的动态内存空间地址,和被扩建之后的内存大小,该函数设计为每次扩容两个struct PeoInfo结构体大小的内存空间。注意realloc()的使用不能为自己对自己扩建。
例如:int* p = realloc(p, 40);
这样写在扩建失败时会丢失原来的地址,让我们再也找不到这一片空间,除非整个项目关闭才能删除这一片空间的内容,具体内容需要你们自己去了解啦^ . ^
正确写法就是写一个中间变量存储新空间,在新空间扩建成功之后再赋值给我们原来哪一个变量。注意容量需要加二。
//检测容量
void Check_Capacity(struct Cont_Book* 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");
}
}
}
(3)添加信息函数
该函数太简单了,就是利用scanf()输入我们的个人信息,在结束时对计数值size加一表示通讯录当中多一个联系人。
//添加
void Add_Contact(struct Cont_Book* ps)
{
//检测容量是否足够
Check_Capacity(ps);
//信息输入
printf("请输入名字》");
scanf("%s", ps->data[ps->size].name);
printf("请输入年龄》");
scanf("%d", &(ps->data[ps->size].age));
printf("请输入性别》");
scanf("%s", ps->data[ps->size].sex);
printf("请输入电话》");
scanf("%s", ps->data[ps->size].tele);
printf("请输入地址》");
scanf("%s", ps->data[ps->size].addr);
ps->size++;
printf("添加成功\n");
}
(4)显示信息函数
显示有两种情况,在通讯录中没有内容时不用显示,需要要提示通讯录为空;有内容时通过计数值size作为循环结束条件,输出每一个联系人的信息。
//显示
void Show_Contact(const struct Cont_Book* ps)
{
if (ps->size == 0)
printf("Empty Contact_Book\n");
else
{
//显示格式
printf("%-10s\t%-4s\t%-6s%-12s%-20s\n", "名字", "年龄", "性别", "电话", "地址");
int i = 0;
for (i = 0; i < ps->size; i++)
{
显示内容
printf("%-10s\t%-4d\t%-6s%-12s%-20s\n",
ps->data[i].name,
ps->data[i].age,
ps->data[i].sex,
ps->data[i].tele,
ps->data[i].addr);
}
}
}
(5)查找下标函数
删除、查找、修改这三个功能都需要找到通讯录当中特定的某一个联系人,所以我们特地封装一个查找下标的函数。
作为找下标函数的前提就是需要知道是依靠什么来匹配的。该函数中利用成员的名字匹配,你们可以改为其它内容,这里并没有硬性条件。所以参数的参数应该为通讯录以及成员名字。
该函数也是利用计数值size遍历匹配,通过strcmp()函数比较通讯录中的名字和我们输入的函数。这里需要注意用法,当完全匹配时strcmp函数返回值为0。找到对应值返回下标,没找到返回-1,用于其它函数操作。
//找成员下标
static int Find_Name_PS(const struct Cont_Book* ps, char name[MAX_NAME])
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
//利用成员名字查找
if (strcmp(ps->data[i].name, name) == 0)
{
//找到,返回下标
return i;
}
}
//没找到,返回-1
return -1;
}
(6)删除信息函数
添加一个判断,在通讯录中没有数据信息时不对通讯录操作。通讯录中有信息时,通我们之前的查找下标函数找到对应值然后删除数据。删除数据有两种方法(就我而言),两种操作并没有区别,复杂程度相同,你们可以任意选择。
第一种是将最后一个联系人填补在被删除联系人的位置,不过该方法会改变我们之前的排序方式,虽然我们有排序手段,但是会多一步操作。
第二种是将被删除联系人后的所有联系人向前移动一位,既不改变原本排序方式,又删除了指定联系人的数据。本次函数的实现就是通过第二种方式实现。
通过之前利用查找下标函数返回的下标作为循环起始条件,表示被删除人之后的哪一位联系人,以计数值size-1作为循环结束条件,因为已经被删除了一个人那么只需要循环size-1-pos次就能够完成操作。
删除成功之后需要对计数值size减一操作,让其与整个通讯录对齐。
//删除
void Del_Contact(struct Cont_Book* ps)
{
char name[MAX_NAME];
if (ps->size == 0)
printf("没有可删除数据\n");
else
{
printf("请输入被删除人的名字》");
scanf("%s", name);
int pos = Find_Name_PS(ps, name); //找下标
int j = 0;
if (pos == -1)
printf("没有对应成员\n");
else
{
for (j = pos; j < ps->size -1; j++)
{
ps->data[j] = ps->data[j + 1];
}
printf("删除成功\n");
ps->size--;
}
}
}
(7)查找信息函数
该函数的实现功能与显示函数几乎相等,只是取消了循环,加入了显示特定人的下标。就能够实现单独某人的信息显示。
//查找
void Search_Contact(const struct Cont_Book* ps)
{
char name[MAX_NAME];
if (ps->size == 0)
printf("没有可查找数据\n");
else
{
printf("请输入被查找人的名字》");
scanf("%s", name);
int pos = Find_Name_PS(ps, name); //找下标
int j = 0;
if (pos == -1)
printf("没有对应成员\n");
else
{
printf("%-10s\t%-4s\t%-6s%-12s%-20s\n", "名字", "年龄", "性别", "电话", "地址");
printf("%-10s\t%-4d\t%-6s%-12s%-20s\n",
ps->data[pos].name,
ps->data[pos].age,
ps->data[pos].sex,
ps->data[pos].tele,
ps->data[pos].addr);
}
}
}
(8)修改信息函数
该函数同样是通过名字查找下标然后找对对应联系人信息,然后逐步输入新的数据,中间没有其它新的内容,不做赘述。
//修改
void Modify_Contact(struct Cont_Book* ps)
{
char name[MAX_NAME];
if (ps->size == 0)
printf("没有可修改数据\n");
else
{
printf("请输入被修改人的名字》");
scanf("%s", name);
int pos = Find_Name_PS(ps, name); //找下标
int j = 0;
if (pos == -1)
printf("没有对应成员\n");
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);
}
}
}
(9)排序信息函数
排序操作我利用了qsort()函数实现操作,具体操作请看我博客内写好的关于qsort()函数的使用,以及仿写一个有相同功能的排序函数。中间有用到回调函数相关知识。
//作为排序的回调函数
int Cmp_Stu_Age(const void* e1, const void* e2)
{
return ((struct PeoInfo*)e1)->age - ((struct PeoInfo*)e2)->age;
}
//排序
void Sort_Contact(struct Cont_Book* ps)
{
if (ps->size <= 1)
printf("没有可排序数据\n");
else
{
//传参内容(通讯录,排序个数,每一个数据字节长度,回调函数名(比较方式))
qsort(ps->data, ps->size, sizeof(ps->data[0]), Cmp_Stu_Age);
}
printf("排序成功\n");
}
(10)保存通讯录数据
该函数利用的文件操作相关的函数,首先创建一个文件,用于存放联系人信息,对于存放文件的指针需要判断是否成功创建。利用循环以计数值size为结束条件,将所有的信息存入进文件当中。最后关闭文件。
//保存通讯录数据
void Save_Contact(struct Cont_Book* ps)
{
//保存文件信息
FILE* pf = fopen("contact.dat", "wb");
if (pf == NULL)
{
//报文件操作失败原因
printf("Save_Contact:%s\n", strerror(errno));
return;
}
//存入数据
int i = 0;
for (i = 0; i < ps->size; i++)
{
//保存通讯录信息
fwrite(&(ps->data[i]), sizeof(struct PeoInfo), 1, pf);
}
fclose(pf);
pf = NULL;
}
(11)加载文件当中的联系人信息函数
功能:当我们文件当中有数据,那么我们会先将这些数据放入我们的通讯录当中。中间利用到了fread()函数,读取文件当中的数据。利用中间变量temp将得到的数据放入通讯录当中,中间调用我们之前自己封装的Check_Capacity()函数在容量不够时增加容量。因为fread()函数在检测到文件没有数据时会返回0,所以利用该函数作为循环体的循环条件。
//加载文件信息
void Load_Contact(struct Cont_Book* ps)
{
//中间变量
struct PeoInfo temp = { 0 };
//读取文件路径
FILE* pf = fopen("contact.dat", "rb");
if (pf == NULL)
{
printf("Load_Contact:%s\n", strerror(errno));
return;
}
//读取通讯录信息
while (fread(&temp, sizeof(struct PeoInfo), 1, pf))
{
//检车容量
Check_Capacity(ps);
ps->data[ps->size] = temp;
ps->size += 1;
}
fclose(pf);
pf = NULL;
}
(12)通讯录使用完毕,销毁开辟的动态内存空间
销毁空间即表示每一次动态内存的开辟都对应一个free()函数操作,防止空间一直占用内存。
//销毁空间
void Destroy_Contact(struct Cont_Book* ps)
{
free(ps->data);
ps->data = NULL;
}
结果: