杜冷丁
楼主
发布于 2023-5-12 13:55:03
阅读 1107
查看全部
目录
准备工作初始化通讯录从文件中加载信息检查容量销毁通讯录添加联络人打印数据删除联络人查找联络人修改联络人排序通讯录保管通讯录总结
本篇博客会讲解如何使用C语言实现一个通讯录。实现通讯录的过程中,会大量用到C语言的知识点,包括但不限于:函数、自定义类型、指针、动态内存管理、文件操作,这些知识点在我的其他博客中都有讲解过,欢送大家阅读,这里就不停止系统的复习了。
先来梳理下需求:
1.通讯录可以存储的联络人的信息有:姓名、年龄、性别、电话、住址。
2.这个通讯录不能是“静态的”,而应该是“动态的”,也就是说,需要用到动态内存管理的知识。这是因为,静态的通讯录的容量是固定的,空间太大可能浪费,太小了又不够存。
3.由于当程序开端运行后,通讯录的数据是存储在内存中的,一旦程序运行完毕,执行完main函数的return 0;后,空间就被操作系统回收了,相当于数据就丢了。为了可以实现“永久保管”的效果,我们要在程序退出前,把数据保管到文件中,这又涉及到文件操作的相关知识点。
4.类似顺序表这种数据构造的根本操作,通讯录要能做到:增删查改+排序+打印,即增加联络人、删除联络人、查找联络人、修改联络人、排序联络人、打印联络人等等。
下面我们开端吧!
准备工作
以下是菜单里的一些选项,声明成枚举类型是比较适宜的。
// 菜单里的不同选项
enum Option
{
EXIT, // 退出
ADD, // 增加联络人
DEL, // 删除联络人
SEARCH, // 查找联络人
MODIFY, // 修改联络人
SHOW, // 显示联络人
SORT // 排序
};由于联络人的姓名、性别、电话和住址都是字符串,要存储在字符数组中,最好先声明它们的容量。
// 各信息的存储容量
#define MAX_NAME 20 // 名字
#define MAX_SEX 5 // 性别
#define MAX_TELE 12 // 电话
#define MAX_ADDR 30 // 住址我们后面在停止动态内存管理时,需要晓得初始的容量和每次扩容的容量,也声明一下:
// 动态内存默认存储的数据
#define DEFAULT_SZ 3
// 若不够存,每次扩容的数量
#define INC_SZ 2再声明一个构造体,表示一个人的信息,包括姓名、年龄、性别、电话、住址。
// 表示一个人的信息
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
{
PeoInfo* data; // data指向了寄存数据的空间
int sz; // 记录通讯录中的有效信息个数
int capacity; // 通讯录当前的容量
}Contact;下面我们开端实现程序的主体逻辑。先从主函数写起,把主要的功能都封装成函数:
// 打印菜单
void menu()
{
printf("************************************\n");
printf("***** 1. add 2. del ***\n");
printf("***** 3. search 4. modify ***\n");
printf("***** 5. show 6. sort ***\n");
printf("***** 0. exit ***\n");
printf("************************************\n");
}
int main()
{
int input = 0; // 存储用户输入的数据
Contact con; // 通讯录
// 初始化通讯录
// 加载文件的信息到通讯录中
InitContact(&con);
do
{
menu(); // 菜单
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
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 EXIT: // 退出通讯录
// 保管通讯录到文件中
SaveContact(&con);
// 销毁通讯录
DestroyConact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
初始化通讯录
先定义一个函数InitContact,它的作用是初始化通讯录。函数的参数是一个指向Contact构造体的指针。函数的详细实现如下:
函数的第一行使用assert宏检查指针是否有效,假设无效则程序会中止运行。接下来,函数使用malloc函数开拓了一块内存空间,用于存储PeoInfo构造体数组。这个数组的大小是DEFAULT_SZ,即默认容量。假设开拓空间失败,则会输出错误信息并返回。假设开拓空间胜利,则将通讯录的大小sz和容量capacity都设置为DEFAULT_SZ,并调用LoadContact函数将文件中的信息加载到通讯录中。
void InitContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 先开拓默认的容量
pc->data = (PeoInfo*)malloc(DEFAULT_SZ * sizeof(PeoInfo));
// 检查开拓空间是否胜利
if (pc->data == NULL)
{
// 开拓空间失败
printf("通讯录初始化失败:%s\n", strerror(errno));
return;
}
// 开拓空间胜利
pc->sz = 0;
pc->capacity = DEFAULT_SZ;
//加载文件的信息到通讯录
LoadContact(pc);
}
从文件中加载信息
再定义一个函数LoadContact,用于从文件中读取联络人信息并存储到内存中。函数的参数是一个指向Contact构造体的指针,表示要将读取到的联络人信息存储到哪个数据构造中。函数的详细实现如下:
首先使用assert函数检查传入的指针是否有效,假设无效则直接返回。然后使用fopen函数翻开名为"contact.dat"的二进制文件,假设翻开失败则说明可能是第一次运行通讯录,没有数据文件,直接返回。接着使用一个while循环,每次读取一个PeoInfo构造体大小的数据,即一个联络人的信息,存储到临时变量tmp中。调用CheckCapacity函数检查当前动态数组的容量是否足够存储读取到的联络人信息,假设不够则停止扩容。将读取到的联络人信息存储到动态数组中,即将tmp变量中的数据存储到data数组的末尾,并将sz变量加一。循环完毕后,关闭文件并将文件指针置为 NULL。
void LoadContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 翻开文件
FILE* pf = fopen("contact.dat", "rb");
// 检查翻开文件是否胜利
if (pf == NULL)
{
// 翻开文件失败,可能是第一次运行通讯录,并没有数据文件
//perror("LoadContact::fopen");
return;
}
// 读文件
PeoInfo tmp = { 0 }; // 存储读取到的数据
// 每次读一个数据
while (fread(&tmp, sizeof(PeoInfo), 1, pf))
{
// 检查容量,不够的话要扩容
CheckCapacity(pc);
// 存储从文件读取到的数据
pc->data[pc->sz] = tmp;
pc->sz++;
}
// 关闭文件
fclose(pf);
pf = NULL;
}
检查容量
再定义一个函数CheckCapacity,用于检查并扩容动态数组。函数的参数是一个指向Contact构造体的指针。函数的详细实现如下:
函数首先使用assert宏检查传入的指针是否有效。然后,它检查当前数组是否需要扩容。假设数组的大小已经等于容量,就需要扩容。在需要扩容的情况下,函数使用realloc函数重新分配内存。realloc函数会尝试将原来分配的内存块扩大到指定的大小。假设扩容胜利,realloc函数会返回一个指向新内存块的指针,否则返回NULL。假设realloc函数返回NULL,说明扩容失败,函数会输出错误信息并返回0。假设realloc函数返回非空指针,说明扩容胜利,函数会更新数组的起始位置和容量,并输出扩容胜利的信息。最后,函数返回1,表示扩容胜利或者不需要扩容。
// 扩容失败,返回0
// 扩容胜利,不需要扩容,返回1
static int CheckCapacity(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 检查是否需要扩容
if (pc->sz == pc->capacity)
{
// 需要扩容
PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SZ) * sizeof(PeoInfo));
// 检查是否扩容胜利
if (ptr == NULL)
{
// 扩容失败
printf("CheckCapacity:%s\n", strerror(errno));
return 0;
}
else
{
// 扩容胜利
// 更新数组的起始位置
pc->data = ptr;
// 更新容量
pc->capacity += INC_SZ;
printf("增容胜利,当前容量:%d\n", pc->capacity);
}
}
return 1;
}
销毁通讯录
再定义一个函数DestroyContact,用来销毁通讯录。函数的参数是一个指向Contact构造体的指针。函数的详细实现如下:
先使用assert宏检查指针的有效性。释放动态数组占用的内存空间。把构造体的变量置空。
void DestroyConact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 释放内存空间
free(pc->data);
pc->data = NULL;
pc->capacity = 0;
pc->sz = 0;
printf("释放内存.....\n");
}
添加联络人
接着定义一个函数AddContact,参数仍然是一个指向Contact构造体的指针,实现向通讯录中添加联络人的功能。详细实现如下:
首先检查传入的指针是否有效,假设无效则使用assert宏触发断言,程序终止。调用CheckCapacity函数检查通讯录是否需要扩容,假设需要则停止扩容操作。假设扩容失败,则输出提示信息并返回。假设扩容胜利,则提示用户输入联络人信息,包括名字、年龄、性别、电话和地址。这些信息将被存储在通讯录的data数组中,下标为 sz。最后更新有效数据个数sz,并输出添加胜利的提示信息。
void AddContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 扩容,同时检查是否胜利
if (0 == CheckCapacity(pc))
{
// 扩容失败
printf("空间不够,扩容失败\n");
return;
}
else
{
// 输入联络人信息
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
// 更新有效数据个数
pc->sz++;
printf("添加胜利\n");
}
}
打印数据
接下来定义一个函数 ShowContact,用于打印联络人信息。函数接受一个指向Contact构造体的指针pc。详细实现如下:
函数首先使用assert宏检查指针pc是否有效,假设无效则程序会解体并输出错误信息。接下来,函数使用printf函数打印联络人信息。首先打印表头,包括姓名、年龄、性别、电话和地址。然后使用循环遍历pc中的每个联络人,打印其姓名、年龄、性别、电话和地址。在打印时使用格式化字符串,其中 %s 表示字符串,%-10s 表示左对齐并占用 10 个字符的字符串,%-4d 表示左对齐并占用 4 个字符的整数,%-5s 表示左对齐并占用 5 个字符的字符串,%-12s 表示左对齐并占用 12 个字符的字符串,%-30s 表示左对齐并占用 30 个字符的字符串。
void ShowContact(const Contact* pc)
{
// 检查指针有效性
assert(pc);
// 打印效果
// 姓名 年龄 性别 电话 地址
// zhangsan 20 男 123456 北京
//
// 打印标题
printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
// 打印数据
for (int i = 0; i < pc->sz; ++i)
{
printf("%-10s %-4d %-5s %-12s %-30s\n",
pc->data.name, pc->data.age, pc->data.sex, pc->data.tele, pc->data.addr);
}
}
删除联络人
接下来实现通讯录的删除联络人功能。详细实现如下:
FindByName函数:根据指定名字,在通讯录中查找联络人信息。函数参数为指向Contact构造体的指针和要查找的名字。函数返回值为查找到的联络人在通讯录中的下标,假设没找到则返回-1。
DelContact函数:删除通讯录中指定联络人。函数参数为指向Contact构造体的指针。函数实现如下:
检查指针有效性,假设为空则直接返回。检查通讯录中是否还有数据,假设没有则输出提示信息并返回。获取用户输入的要删除的联络人名字。调用FindByName函数查找要删除的联络人在通讯录中的下标。假设没找到,则输出提示信息并返回。假设找到了,则使用memmove函数将该联络人后面的所有联络人向前挪动一个位置,覆盖掉要删除的联络人。更新通讯录中的有效数据个数。输出删除胜利的提示信息。
// 根据指定名字,查找联络人信息
static int FindByName(const Contact* pc, char name[])
{
// 检查指针有效性
assert(pc);
assert(name);
// 遍历数组
for (int i = 0; i < pc->sz; ++i)
{
// 检查名字是否匹配
if (0 == strcmp(pc->data.name, name))
{
// 找到了
return i;
}
}
// 没找到
return -1;
}
void DelContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 检查是否还有数据
if (pc->sz == 0)
{
// 通讯录已空
printf("通讯录为空,无法删除\n");
return;
}
char name[MAX_NAME] = { 0 }; // 存储用户输入的数据
// 用户输入信息
printf("输入要删除人的名字:>");
scanf("%s", name);
// 找到要删除的人的下标
int pos = FindByName(pc, name);
// 检查是否找到
if (pos == -1)
{
// 没找到
printf("要删除的人不存在\n");
return;
}
// 删除pos位置上的数据
// 挪动pos后面的数据,覆盖pos
memmove(pc->data + pos, pc->data + pos + 1, sizeof(PeoInfo) * (pc->sz - pos - 1));
// 更新有效数据个数
pc->sz--;
printf("删除胜利\n");
}
查找联络人
接着实如今通讯录中根据姓名查找联络人的功能。详细实现如下:
函数名为SearchContact,接受一个指向Contact构造体的指针pc作为参数。第一行代码使用assert宏检查指针pc是否有效,假设pc为 NULL,则程序会解体并输出错误信息。定义一个char类型的数组name,长度为MAX_NAME,用于存储用户输入的要查找的人的名字。使用printf函数输出提示信息,让用户输入要查找的人的名字。使用scanf函数读取用户输入的名字,存储到name数组中。调用FindByName函数,在通讯录中查找名字为name的联络人,返回值为该联络人在通讯录中的位置,假设没找到则返回-1。判断FindByName函数的返回值,假设为-1,则说明没有找到要查找的联络人,使用printf函数输出提示信息,并直接返回。假设FindByName函数的返回值不为-1,则说明找到了要查找的联络人,使用printf函数输出通讯录的标题行,包括姓名、年龄、性别、电话、地址等信息。使用printf函数输出找到的联络人的详细信息,包括姓名、年龄、性别、电话、地址等信息,这些信息都存储在Contact构造体中的data数组中,通过pc指针访问。pos变量表示要查找的联络人在data数组中的位置。注意,这里使用了%-10s、%-4d等格式控制符,表示输出字符串时左对齐,并且占用固定的宽度,方便对齐。
void SearchContact(const Contact* pc)
{
// 检查指针有效性
assert(pc);
char name[MAX_NAME] = { 0 }; // 存储用户输入的信息
// 用户输入数据
printf("请输入要查找人的名字:>");
scanf("%s", name);
// 查找
int pos = FindByName(pc, name);
// 检查是否找到
if (pos == -1)
{
// 没找到
printf("要查找的人不存在\n");
return;
}
// 打印标题行
printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
// 打印数据
printf("%-10s %-4d %-5s %-12s %-30s\n",
pc->data[pos].name,
pc->data[pos].age,
pc->data[pos].sex,
pc->data[pos].tele,
pc->data[pos].addr);
}
修改联络人
再下来实现一个函数,函数的参数仍然是一个指向通讯录构造体类型Contact的指针,用于修改通讯录中的联络人信息。详细实现如下:
使用assert宏函数检查指针有效性,假设指针为空,则程序会终止。定义一个char类型数组name,用于存储用户输入的联络人名字。使用printf函数提示用户输入要修改的联络人名字,并使用scanf函数读取用户输入的名字。调用FindByName函数查找通讯录中是否存在该联络人,假设不存在则输出提示信息并返回。假设存在该联络人,则使用scanf函数分别读取用户输入的联络人信息,包括名字、年龄、性别、电话和地址,并将这些信息存储到通讯录构造体中对应的位置。最后使用printf函数输出修改胜利的提示信息。
void ModifyContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
char name[MAX_NAME] = { 0 }; // 存储用户输入的信息
// 用户输入数据
printf("请输入要修改人的名字:>");
scanf("%s", name);
// 查找
int pos = FindByName(pc, name);
// 检查是否找到
if (pos == -1)
{
// 没找到
printf("要修改的人不存在\n");
return;
}
// 修改
// 用户输入信息
printf("请输入名字:>");
scanf("%s", pc->data[pos].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pos].age));
printf("请输入性别:>");
scanf("%s", pc->data[pos].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pos].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pos].addr);
printf("修改胜利\n");
}
排序通讯录
接下来实现排序功能。详细实现如下:
定义一个比较函数CmpByName,用于依照名字来排序联络人信息。该函数的参数为两个指向联络人信息构造体的指针p1和p2,返回值为两个名字字符串的比较结果。在CmpByName函数中,首先使用assert宏检查指针p1和p2的有效性,确保程序不会因为无效指针而解体。然后使用strcmp函数比较两个联络人信息构造体中的名字字符串大小,返回比较结果。定义一个排序函数SortContact,用于对联络人信息停止排序。该函数的参数为一个指向联络人管理系统构造体的指针pc。在SortContact函数中,首先使用assert宏检查指针pc的有效性,确保程序不会因为无效指针而解体。然后调用qsort函数对联络人信息停止排序,其中pc->data表示联络人信息数组的首地址,pc->sz表示联络人信息数组的大小,sizeof(PeoInfo)表示每个联络人信息构造体的大小,CmpByName表示排序函数。最后输出排序胜利的提示信息。
// 依照名字来排序
int CmpByName(const void* p1, const void* p2)
{
// 检查指针有效性
assert(p1 && p2);
// 比较名字字符串大小
return strcmp(((PeoInfo*)p1)->name, ((PeoInfo*)p2)->name);
}
void SortContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 根据名字来排序
qsort(pc->data, pc->sz, sizeof(PeoInfo), CmpByName);
printf("排序胜利\n");
}
保管通讯录
最后实现将联络人信息保管到文件中的功能。详细实现如下:
函数定义:函数名为SaveContact,参数为一个指向Contact构造体的指针pc。检查指针有效性:使用assert宏函数检查指针pc是否为NULL,假设是NULL则程序会直接终止。翻开文件:使用fopen函数翻开名为"contact.dat"的文件,以二进制写入形式(“wb”)翻开。返回值为一个指向FILE构造体的指针pf。检查是否翻开胜利:使用if语句判断指针pf是否为NULL,假设是NULL则说明翻开文件失败,使用perror函数输出错误信息并返回。写数据:使用for循环遍历pc指向的Contact构造体中的所有联络人信息,使用fwrite函数将每个联络人信息写入文件中。其中,第一个参数为指向联络人信息的指针,第二个参数为每个联络人信息的大小,第三个参数为写入的数量,第四个参数为指向文件的指针pf。关闭文件:使用fclose函数关闭文件,释放文件指针pf所占用的资源。将pf赋值为NULL,防止呈现野指针。输出保管胜利信息:使用printf函数输出保管胜利的信息。
void SaveContact(Contact* pc)
{
// 检查指针有效性
assert(pc);
// 翻开文件
FILE* pf = fopen("contact.dat", "wb");
// 检查是否翻开胜利
if (pf == NULL)
{
// 翻开文件失败
perror("SaveContact::fopen");
return;
}
// 写数据,一次写一个
for (int i = 0; i < pc->sz; ++i)
{
fwrite(pc->data + i, sizeof(struct PeoInfo), 1, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
printf("保管胜利...\n");
}
总结
动态内存管理一定要熟练掌握,包括使用malloc开拓空间,使用realloc扩容,使用free函数释放空间等。常见的文件操作函数得会用,包括fopen,fclose以及文件的顺序读写和随机读写操作等。顺序表的增删查改、排序打印等常规操作一定要掌握,通讯录的实质就是顺序表。
以上就是基于C语言打造高效通讯录的示例代码的详细内容,更多关于C语言通讯录的资料请关注网站其它相关文章! |
|