【C语言】通讯录系统实现 (保姆级教程,附源码)

简介: 【C语言】通讯录系统实现 (保姆级教程,附源码)

1、通讯录系统介绍

实现一个通讯录:

  1. 可以保存100个人的信息(后续优化成动态开辟)
  2. 增加人的信息
  3. 删除指定联系人的信息
  4. 查询指定联系人的信息
  5. 修改指定联系人的信息
  6. 排序通讯录的信息
  7. 显示所有联系人的信息

其中,人的信息包括:名字、年龄、性别、电话 、地址

2、代码分装

源文件:

test.c: 测试通讯录的基本功能

game.c: 实现通讯录的相关函数

头文件:

game.h: 相关函数和类型的声明,整个代码要引用的头文件以及宏定义

3、代码实现步骤

3.1、制作菜单menu函数以及游戏运行逻辑流程

  • 首先在 test.c 中定义一个menu函数打印菜单,提示玩家进行选择。
  • scanf接收玩家输入并用switch判断,针对判断进行相应操作,输入错误时提示选择错误,重新选择。而为了能够让用户重新选择以及完成一个功能操作时再继续下一个功能操作,需要使用do while语句将它们包含起来。

使用枚举一一列举菜单选择的可能取值,便于后续使用时能够见名知意,增加代码可读性。

【test.c】

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");
}
enum Option  //枚举常量
{
  EXIT,
  ADD,
  DEL,
  SEARCH,
  MODIFY,
  SHOW,
  SORT
};
int main()
{
  int input = 0;
  do
  {
    menu();
    printf("请输入你的选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case ADD:
      break;
    case DEL:
      break;
    case SEARCH:
      break;
    case MODIFY:
      break;
    case SHOW:
      break;
    case SORT:
      break;
    case EXIT:
      printf("退出通讯录\n");
      break;
    default:
      printf("选择错误,重新选择\n");
      break;
    }
  } while (input);
  return 0;
}

3.2、封装人的信息PeoInfo以及通讯录Contact结构体类型

创建结构体类型PeoInfo用于描述人的信息,然后再创建一个Contact结构体包含PeoInfo和个数,这样一个通讯录结构体类型便完成了 。

为了方便后续代码的修改,这里将所有常量都用 #define 定义了变量。

【contact.h】

#define NAME_MAX 20
#define SEX_MAX 5
#define TELE_MAX 12
#define ADDR_MAX 30
#define MAX 100
typedef struct PeoInfo
{
  char name[NAME_MAX];
  int age;
  char sex[SEX_MAX];
  char tele[TELE_MAX];
  char addr[ADDR_MAX];
}PeoInfo;
typedef struct Contact
{
  PeoInfo data[MAX];
  int sz; //记录的是当前通讯录中存放的人的信息个数
}Contact;

3.3、初始化通讯录InitContact函数

【contact.h】

//初始化通讯录
void InitContact(Contact* pc);

【contact.c】

assert断言,对传入的指针进行判断,防止对空指针进行操作。

使用 memset函数 初始化结构体中的数据。针对memset函数,如果不了解其原理,可以前往往期博客进行学习了解:

void InitContact(Contact* pc)
{
  assert(pc);
  pc->sz = 0;
  memset(pc->data, 0, sizeof(pc->data));
}

【test.c】

int main()
{
  int input = 0;
    Contact con; //通讯录
  InitContact(&con);//初始化通讯录
  do
  {
    menu();
    printf("请输入你的选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case ADD:
      break;
    case DEL:
      break;
    case SEARCH:
      break;
    case MODIFY:
      break;
    case SHOW:
      break;
    case SORT:
      break;
    case EXIT:
      printf("退出通讯录\n");
      break;
    default:
      printf("选择错误,重新选择\n");
      break;
    }
  } while (input);
  return 0;
}

3.4、增加联系人AddContact函数

【contact.h】

//增加联系人
void AddContact(Contact* pc);

【contact.c】

assert断言,对传入的指针进行判断,防止对空指针进行操作。

需要判断当sz不等于MAX时再继续进行添加操作,防止溢出。

此处需要注意的是,人的信息中只有age是整型,其他的name、sex、tele、addr都为字符数组,scanf在接收age的时候需要取出age的地址,即 &(pc->data[pc->sz].age)。

void AddContact(Contact* pc)
{
  assert(pc);
  if (pc->sz == MAX)
  {
    printf("通讯录已满,无法增加\n");
    return;
  }
    //增加信息
  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 用于显示所有联系人的信息。

3.5、显示所有联系人ShowContact函数

【contact.h】

虽然显示操作不需要修改通讯录中的内容,但是出于效率和空间的考虑,还是选择传址,然后使用const修饰。

//显示所有联系人
void ShowContact(const Contact* pc);

【contact.c】

注意11行和14行的不同。

因为11行打印的标题行都是字符串,所以都是用了%s,而14行的信息行中年龄是整型的 ,因此需要使用%d打印,需要注意这里的不同。

void ShowContact(const Contact* pc)
{
  assert(pc);
  if (pc->sz == 0)
  {
    printf("通讯录为空,无需打印\n");
    return;
  }
  int i = 0;
  printf("%-20s%-5s%-5s%-12s%-30s\n", "名字", "年龄", "性别", "电话", "地址");  //标题行
  for ( i = 0; i < pc->sz; i++)
  {
    printf("%-20s%-5d%-5s%-12s%-30s\n",
      pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
  }
}

【test.c】

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");
}
enum Option
{
  EXIT,
  ADD,
  DEL,
  SEARCH,
  MODIFY,
  SHOW,
  SORT
};
int main()
{
  int input = 0;
  Contact con; //通讯录
  InitContact(&con);//初始化通讯录
  do
  {
    menu();
    printf("请输入你的选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case ADD:
      AddContact(&con);
      break;
    case DEL:
      break;
    case SEARCH:
      break;
    case MODIFY:
      break;
    case SHOW:
      ShowContact(&con);
      break;
    case SORT:
      break;
    case EXIT:
      printf("退出通讯录\n");
      break;
    default:
      printf("选择错误,重新选择\n");
      break;
    }
  } while (input);
  return 0;
}

下面试着添加联系人并打印通讯录:

3.6、删除联系人DelContact函数以及判断是否存在FindByName函数

【contact.h】

//删除联系人
void DelContact(Contact* pc);

【contact.c】

  • 因为通过名字判断此人是否存在的FindByName函数这个功能在其他操作上也需要使用到,所以最好将它封装成一个函数,减少代码冗余并且提高写代码效率。
  • 当FindByName函数在通讯录中找到此人时返回此人在data数组中的下标,找不到是返回-1。
  • 又因为FindByName函数只在contact.c中被使用,因此可以加上static关键字修饰。
static int FindByName(Contact* pc, char name[])
{
  assert(pc);
  int i = 0;
  for ( i = 0; i < pc->sz; i++)
  {
    if (strcmp(pc->data[i].name, name) == 0)  //两个字符串比较用strcmp
      return i;
  }
  return -1;
}
void DelContact(Contact* pc)
{
  char name[NAME_MAX];
  assert(pc);
  if (pc->sz == 0)
  {
    printf("通讯录为空,无法删除\n");
    return;
  }
  printf("请输入要删除人的名字:");
  scanf("%s", name);
  //查找名字为name的人
  int ret = FindByName(pc, name);
  if (ret == -1)
  {
    printf("要删除的人不存在\n");
    return;
  }
  //删除这个人
  int i = 0;
  for (i = ret; i < pc->sz - 1; i++)    //注意此处的sz - 1 
  {
    pc->data[i] = pc->data[i + 1];
  }
  pc->sz--;
  printf("删除成功\n");
}

【删除操作图解】

这里以0~6的下标作为例子,假如ret返回的下标时1。

只需要将ret后面的所有内容向前平移一位,就可以覆盖掉要ret指向的内容,然后再对sz--,这样变相地完成了对信息的删除。等将来又有新联系人加入时,因为sz--了所以写入信息时会覆盖掉下标为6中的内容。

此时使用该函数尝试删除一下联系人,结果是可以的。

3.7、查找指定联系人SearchContact函数

【contact.h】

//查找指定联系人
void SearchContact(Contact* pc);

【contact.c】

依然是调用FindByName函数进行判断,有此人则返回下标,没有此人则返回-1。

找到之后直接打印就行。

void SearchContact(Contact* pc)
{
  char name[NAME_MAX];
  assert(pc);
  printf("请输入要查找人的名字:");
  scanf("%s", name);
  int ret = FindByName(pc, name);
  if (ret == -1)
  {
    printf("要查找的人不存在\n");
    return;
  }
  //显示查查找到的信息
  printf("%-20s%-5s%-5s%-12s%-30s\n", "名字", "年龄", "性别", "电话", "地址");
  printf("%-20s%-5d%-5s%-12s%-30s\n",
    pc->data[ret].name, pc->data[ret].age, pc->data[ret].sex, pc->data[ret].tele, pc->data[ret].addr);
}

3.8、修改指定联系人ModifyContact函数

【contact.h】

//修改指定联系人
void ModifyContact(Contact* pc);

【contact.c】

依然是调用FindByName函数进行判断,有此人则返回下标,没有此人则返回-1。

找到之后直接将该下标下的所有信息都重新接收并覆盖,就完成了修改操作。

void ModifyContact(Contact* pc)
{
  char name[NAME_MAX];
  assert(pc);
  printf("请输入要修改人的名字:");
  scanf("%s", name);
  int ret = FindByName(pc, name);
  if (ret == -1)
  {
    printf("要修改的人不存在\n");
    return;
  }
  //修改
  printf("请输入名字:");
  scanf("%s", pc->data[ret].name);
  printf("请输入年龄:");
  scanf("%d", &(pc->data[ret].age));
  printf("请输入性别:");
  scanf("%s", pc->data[ret].sex);
  printf("请输入电话:");
  scanf("%s", pc->data[ret].tele);
  printf("请输入地址:");
  scanf("%s", pc->data[ret].addr);
}

3.9、以年龄排序联系人SortContact函数

【contact.h】

//排序联系人
void SortContact(Contact* pc);

【contact.c】

qsort可以对任意数据进行排序,因此使用qsort以年龄为关键字进行排序。

关于 qsort函数 我在往期的博客中有进行详细讲解,这里由于篇幅原因,就不过多赘述,如果不懂或者想要更深入了解的话,可以前往:

int cmp_age(const void* e1, const void* e2)
{
  return ((PeoInfo*)e1)->age - ((PeoInfo*)e2)->age;
}
void SortContact(Contact* pc)
{
  assert(pc);
  if (pc->sz == 0)
  {
    printf("通讯录为空,无需排序\n");
    return;
  }
  qsort(pc, pc->sz, sizeof(PeoInfo), cmp_age);
  printf("排序成功\n");
}

到这里通讯录的实现就基本完成了,但是有些读者可以注意到,这个通讯录还存在一些问题:

  1. 录入的信息,等程序结束后就不存在了,这是因为数据存放在内存中的。为了解决这个问题,需要使用到文件存储的知识。
  2. 通讯录的大小是固定的100个元素,只能最多存放100个人。当信息太少时,就会导致空间剩余过大浪费空间,而当信息太多时空间又太小了无法进行存入,而解决这个问题需要使用到动态内存管理的知识。下面就来优化一下通讯录。

4、使用动态规划优化通讯录

规定:

  1. 通讯录刚开始时可以存放3个人的信息。#define DEFAULT_SZ 3
  2. 当空间放满时,自动增加容量,每次增加2个信息的空间。#define DEFAULT_INC 2

定义通讯录Contact结构体:

因为数组空间是固定的,因此这里改成了指针,后续初始化时对data指针进行calloc动态开辟空间,并且增加用于记录当前最大容量的变量capacity

静态版本
//typedef struct Contact
//{
//  PeoInfo data[MAX];
//  int sz; //记录的是当前通讯录中存放的人的信息个数
//}Contact;
//动态版本
typedef struct Contact
{
  PeoInfo* data;
  int sz; //记录的是当前通讯录中存放的人的信息个数
    int capacity; //记录的时通讯录当前的最大容量
}Contact;

初始化通讯录InitContact函数:

此时就可以使用calloc对data指针进行动态开辟空间,如果开辟失败则用perror打印错误信息。

对于perror函数的使用以及注意事项,在往期博客中有进行详细讲解,这就不过多赘述,感兴趣的可以前往:

静态版本
//void InitContact(Contact* pc)
//{
//  assert(pc);
//  pc->sz = 0;
//  memset(pc->data, 0, sizeof(pc->data));
//}
//动态版本
void InitContact(Contact* pc)
{
  assert(pc);
  pc->sz = 0;
  pc->capacity = DEFAULT_SZ;  //#define DEFAULT_SZ 3
  pc->data = (PeoInfo*)calloc(pc->capacity, sizeof(PeoInfo));
  if (pc->data == NULL)
  {
    perror("InitContact->calloc");
    return;
  }
}

增加联系人AddContact函数:

因为此时使用了动态规划,所以不会存在通讯录满了无法增加的情况,此时if语句改为判断是否已满,如果已满则使用realloc增容。此时需要用另一个指针ptr接收realloc的空间,判断ptr指向的空间是否开辟成功,成功时再将ptr赋值给pc->data,防止数据丢失。

静态版本
//void AddContact(Contact* pc)
//{
//  assert(pc);
//  if (pc->sz == MAX)
//  {
//    printf("通讯录已满,无法增加\n");
//    return;
//  }
//  
//  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");
//
//}
//动态版本
void AddContact(Contact* pc)
{
  assert(pc);
  if (pc->sz == pc->capacity)
  {
    PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + DEFAULT_INC) * sizeof(PeoInfo));
    if (ptr != NULL)
    {
      pc->data = ptr;
      pc->capacity += DEFAULT_INC;  //#define DEFAULT_INC 2
            printf("增容成功\n");
    }
    else
    {
      perror("AddContact->realloc");
      return;
    }
  }
  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");
}

在EXIT退出通讯录时候记得对动态开辟的空间进行free操作:

定义一个DestroyContact函数用于free掉动态开辟的空间。

【contact.h】

//销毁通讯录
void DestroyContact(Contact* pc);

【contact.c】

void DestroyContact(Contact* pc)
{
  free(pc->data);
  pc->data = NULL;
  pc->sz = 0;
  pc->capacity = 0;
}

5、通讯录系统完整代码

源码已经上传至gitee中,有需要可以前往领取:Contact/Contact · Nadez/Study_c - 码云 - 开源中国 (gitee.com)

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

目录
相关文章
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
1288 10
|
存储 安全 数据管理
C语言之考勤模拟系统平台(千行代码)
C语言之考勤模拟系统平台(千行代码)
346 4
|
IDE 编译器 开发工具
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
在本文中,我们系统地讲解了常见的 `#pragma` 指令,包括其基本用法、编译器支持情况、示例代码以及与传统方法的对比。`#pragma` 指令是一个强大的工具,可以帮助开发者精细控制编译器的行为,优化代码性能,避免错误,并确保跨平台兼容性。然而,使用这些指令时需要特别注意编译器的支持情况,因为并非所有的 `#pragma` 指令都能在所有编译器中得到支持。
1763 41
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
|
监控 Linux PHP
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
574 20
|
存储 编译器 C语言
【C语言】C语言的变量和声明系统性讲解
在C语言中,声明和定义是两个关键概念,分别用于告知编译器变量或函数的存在(声明)和实际创建及分配内存(定义)。声明可以多次出现,而定义只能有一次。声明通常位于头文件中,定义则在源文件中。通过合理组织头文件和源文件,可以提高代码的模块化和可维护性。示例包括全局变量、局部变量、函数、结构体、联合体、数组、字符串、枚举和指针的声明与定义。
849 12
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
845 16
|
监控 关系型数据库 MySQL
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
603 0
|
机器学习/深度学习 算法 数据挖掘
C语言在机器学习中的应用及其重要性。C语言以其高效性、灵活性和可移植性,适合开发高性能的机器学习算法,尤其在底层算法实现、嵌入式系统和高性能计算中表现突出
本文探讨了C语言在机器学习中的应用及其重要性。C语言以其高效性、灵活性和可移植性,适合开发高性能的机器学习算法,尤其在底层算法实现、嵌入式系统和高性能计算中表现突出。文章还介绍了C语言在知名机器学习库中的作用,以及与Python等语言结合使用的案例,展望了其未来发展的挑战与机遇。
557 1
|
人工智能 安全 算法
基于C语言的嵌入式系统开发,涵盖嵌入式系统概述、C语言的优势、开发流程、关键技术、应用实例及面临的挑战与未来趋势。
本文深入探讨了基于C语言的嵌入式系统开发,涵盖嵌入式系统概述、C语言的优势、开发流程、关键技术、应用实例及面临的挑战与未来趋势。C语言因其高效、可移植、灵活及成熟度高等特点,在嵌入式系统开发中占据重要地位。文章还介绍了从系统需求分析到部署维护的完整开发流程,以及中断处理、内存管理等关键技术,并展望了嵌入式系统在物联网和人工智能领域的未来发展。
851 1
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
639 7