目录
学完了C语言的基础语法、数组、函数、指针、自定义类型、动态内存分配、文件操作等知识后算是掌握了一些C基础,这时候写写小项目倒是一个不错的巩固方法,本文就来分享一波用C语言简单实现三个版本的通讯录小程序(源码放在文末了)。
如果有尚未掌握的知识点最好先去学习了再来阅读本文,欢迎读者来我的C语言专栏参考参考。
由于笔者水平有限,难免存在纰漏,读者各取所需即可。
1.通讯录中能存放1000个人的信息
2.每个人的信息包括:姓名+年龄+性别+电话+地址
3.增删查改指定人的信息
4.能对通讯录信息排序
5.可以打印出所需信息
6.随时退出
用三个文件实现,一个是运行主函数的文件test.c,一个是存放通讯录具体功能实现函数的文件contact.c,另一个就是对应头文件contact.h。
因为每次操作完都需要不断刷新状态,所以用do while循环。
初始化一下菜单,定义menu函数,1-6分别对应增删查改和排序、打印,0则为退出。
- void menu()
- {
- printf("************************************************\n");
- printf("************* 1.add 2.del ***********\n");
- printf("************* 3.search 4.modify ***********\n");
- printf("************* 5.sort 6.print ***********\n");
- printf("************* 0.exit ***********\n");
- printf("************************************************\n");
- printf("************************************************\n");
-
- }
需要根据用户输入决定下一步操作,用input接收输入值,switch()对应7种情况。
定义了一个枚举,如果单单用case 1:之类的语句不清楚到底对应哪一个操作,而使用枚举常量就可以很清楚input的值到底对应哪一个操作了。
- //防止忘记选项对应操作
- enum Option
- {
- EXIT,
- ADD,
- DEL,
- SEARCH,
- MODIFY,
- SORT,
- PRINT
-
- };
- int main()
- {
- int input = 0;
- do
- {
- menu();
- printf("请选择:>");
- scanf_s("%d",&input);
- switch (input)
- {
- case EXIT:
- break;
- case ADD:
- break;
- case DEL:
- break;
- case SEARCH:
- break;
- case MODIFY:
- break;
- case PRINT:
- break;
- case SORT:
- break;
- default:
- break;
-
- }
-
-
- } while (input);
-
-
- system("pause");
- }
要存放多个人的信息,并且每个人的信息都具有这五个属性,只是对应值各不相同,所以用结构体来封装变量,多人就用结构体数组。
用宏定义常量,方便修改。
为什么又用了一层结构体封装呢?为的是加上一个记录通讯录已存储联系人个数的变量,类似于索引。
对于结构类型定义,因为两个c文件都要使用对应结构,所以把它放在头文件contact.h中。
- #define MAX_NAME 20
- #define MAX_SEX 10
- #define MAX_TELE 12
- #define MAX_ADDR 30
- #define MAX 1000
-
- //结构类型定义--个人信息
- struct PeoInfo
- {
- char name[MAX_NAME];
- char sex[MAX_SEX];
- int age;
- char tele[MAX_TELE];
- char addr[MAX_ADDR];
- };
-
- //通讯录结构
- typedef struct Contact
- {
- struct PeoInfo data[MAX];//存放信息
- int size;//记录当前通讯录中有效信息个数
- }Contact;
然后根据功能需求设计函数,传结构体地址给每个功能函数(更高效)。
每个函数定义放在contact.c,声明放在contact.h。
使用memset函数把整个结构初始化为0。
- //初始化通讯录
- void InitContact(Contact*pc)
- {
- //全部置零
- memset(pc, 0, sizeof(pc));
- }
利用size作为索引,size就是目前已存入联系人个数和下一个要存储的联系人次序,比如size == 5代表现在将要录入的是第五个人的信息。结构数组data以size为下标,每增加一个人的信息后size++。
- //增加联系人
- void AddContact(Contact* pc)
- {
- //首先判断通讯录是否已满
- if (pc->size == MAX)
- {
- printf("通讯录已满,无法再添加联系人!\n");
- return;//无返回值但是可以用来结束函数
- }
- //增加一个人的信息
- printf("请输入姓名:>");
- scanf("%s",pc->data[pc->size].name);
- getchar();
- printf("请输入年龄:>");
- scanf("%d",&(pc->data[pc->size].age));
- printf("请输入性别:>");
- scanf("%s", pc->data[pc->size].sex);
- getchar();
- printf("请输入电话:>");
- scanf("%s", pc->data[pc->size].tele);
- getchar();
- printf("请输入地址:>");
- scanf("%s", pc->data[pc->size].addr);
- getchar();
-
- pc->size++;//索引+1
- printf("增加成功!\n");
-
- }
为了先看看上一个函数录入信息效果如何,我们先写一下打印函数。
- //打印联系人信息
- void PrintContact(const Contact* pc)//常量指针防修改,只是打印操作并不需要改变原结构体
- {
- int i;
- printf("%-15s\t%-5s\t%-5s\t%-12s\t%-20s\n","姓名","年龄","性别","电话","地址");
- //打印所有联系人信息
- for (i = 0; i < pc->size; i++)
- {
- printf("%-15s\t%-5d\t%-5s\t%-12s\t%-20s\n", pc->data[i].name,
- pc->data[i].age,
- pc->data[i].sex,
- pc->data[i].tele,
- pc->data[i].addr);//为了看着舒服
-
- }
- }
删除的前提是什么?是通讯录里已经存在信息才能删除,所以最开始要判断通讯录是否为空。
我们要删除选定的联系人,应该先找到该联系人信息所在位置,然后再执行删除操作。
所以要先写一个依据联系人姓名信息查找联系人的函数。然后注意如何删除联系人,这里采用的方法是让后面每一个人的信息覆盖掉前一个人的信息,一直到最后一个覆盖掉倒数第二个人信息,再把size减小1也就是存储总个数-1,原先最后一个人的信息就相当于被屏蔽掉了(数据仍然存在,若增加联系人则可以将其覆盖)。

- //查找联系人(依据姓名,仅在本源文件中使用)
- static int FindByName(Contact* pc, char name[])
- {
- int i = 0;
- for (i = 0; i < pc->size; i++)
- {
- //利用字符串比较函数,为0则找到,返回对应数组下标
- if (strcmp(pc->data[i].name, name) == 0)
- return i;
- }
- return -1;
- }
-
- //删除联系人
- void DelContact(Contact* pc)
- {
- char name[MAX_NAME];
- //首先判断通讯录是否为空
- if (pc->size == 0)
- {
- printf("通讯录空空如也呢,无法删除哦(* ̄3 ̄)╭");
- return;//结束函数
- }
- printf("请输入要删除的人的姓名:>");
- scanf("%s", name);
- //1.查找联系人(依据姓名)
- int pos = FindByName(pc, name);//返回数组下标
- //找不到联系人
- if (pos == -1)
- {
- printf("查无此人哦,换一个试试吧!\n");
- return;
- }
- //2.删除联系人
- int i = 0;
- for (i = pos; i < pc->size - 1; i++)
- {
- //把下标pos后面的信息向前挪
- pc->data[i] = pc->data[i + 1];
- }
-
- //联系人总个数-1,原最后一位联系人信息相当于被屏蔽
- pc->size--;
- printf("删除成功!\n");
- }
查找并打印目标联系人的信息,这里以FindByName()函数为基础直接就可以构建函数。
- //查找联系人(依据姓名并打印信息)
- void SearchContact(Contact* pc)
- {
- char name[MAX_NAME];
- printf("请输入要查找的联系人的姓名:>");
- scanf("%s",name);
- getchar();
- //1.查找联系人(依据姓名)
- int pos = FindByName(pc, name);//返回数组下标
- //找不到联系人
- if (pos == -1)
- {
- printf("查无此人哦,换一个试试吧!\n");
- return;
- }
- else
- {
- //2.打印目标联系人信息
- printf("%-15s\t%-5s\t%-5s\t%-12s\t%-20s\n", "姓名", "年龄", "性别", "电话", "地址");
- printf("%-15s\t%-5d\t%-5s\t%-12s\t%-20s\n", pc->data[pos].name,
- pc->data[pos].age,
- pc->data[pos].sex,
- pc->data[pos].tele,
- pc->data[pos].addr);
- }
-
- }
修改分为单项修改和全部都修改,而修改前需要先查找联系人,可以调用FindByName(),找到的话先打印目标的信息,再选择单项还是全部修改,这里定义char modification[5]来接收用户的选择,再用strcmp()一个个比对,修改时size不变,直接在指定位置覆盖掉原联系人信息即可。
- //修改联系人信息
- void ModifyContact(Contact* pc)
- {
- char name[MAX_NAME];
- char modification[5];
- printf("请输入要修改的联系人的姓名:>");
- scanf("%s", name);
- getchar();
-
- //1.查找联系人(依据姓名)
- int pos = FindByName(pc, name);//返回数组下标
- //找不到联系人
- if (pos == -1)
- {
- printf("查无此人哦,换一个试试吧!\n");
- return;
- }
- else
- {
- //2.打印目标联系人信息
- printf("%-15s\t%-5s\t%-5s\t%-12s\t%-20s\n", "姓名", "年龄", "性别", "电话", "地址");
- printf("%-15s\t%-5d\t%-5s\t%-12s\t%-20s\n", pc->data[pos].name,
- pc->data[pos].age,
- pc->data[pos].sex,
- pc->data[pos].tele,
- pc->data[pos].addr);
-
- //3.修改目标联系人的信息
- printf("输入中文,单项(如姓名等)或者全部\n");
- printf("请输入要修改的选项:>");
- scanf("%s",modification);
- getchar();
- //全部修改
- if (strcmp(modification, "全部") == 0)
- {
- printf("请输入姓名:>");
- scanf("%s", pc->data[pos].name);
- getchar();
- printf("请输入年龄:>");
- scanf("%d", &(pc->data[pos].age));
- printf("请输入性别:>");
- scanf("%s", pc->data[pos].sex);
- getchar();
- printf("请输入电话:>");
- scanf("%s", pc->data[pos].tele);
- getchar();
- printf("请输入地址:>");
- scanf("%s", pc->data[pos].addr);
- getchar();
- }
- //单项修改
- if (strcmp(modification, "姓名") == 0)
- {
- printf("请输入姓名:>");
- scanf("%s", pc->data[pos].name);
- getchar();
- }
- if (strcmp(modification, "年龄") == 0)
- {
- printf("请输入年龄:>");
- scanf("%d", &(pc->data[pos].age));
- }
- if (strcmp(modification, "性别") == 0)
- {
- printf("请输入性别:>");
- scanf("%s", pc->data[pos].sex);
- getchar();
- }
- if (strcmp(modification, "电话") == 0)
- {
- printf("请输入电话:>");
- scanf("%s", pc->data[pos].tele);
- getchar();
-
- }
- if (strcmp(modification, "地址") == 0)
- {
- printf("请输入地址:>");
- scanf("%s", pc->data[pos].addr);
- getchar();
- }
- printf("修改成功!\n");
- }
-
- }
我们在存入联系人信息后,如果想要按照某种标准来排序联系人信息的话该怎么做呢?可以使用qsort函数。这里只根据姓名来排序。
- static int CmpContactByName(const void* n1, const void* n2)
- {
- return strcmp(((PeoInfo *)n1)->name, ((PeoInfo*)n2)->name);
- }
-
- void SortContact(Contact* pc)
- {
- if (0 == pc->size)
- {
- printf("通讯录空空如也哦,无法排序捏~\n");
- return;
- }
-
- qsort(pc->data, pc->size, sizeof(PeoInfo), CmpContactByName);
-
- printf("根据姓名来排序......排序成功!\n");
- }
不知道读者会不会觉得一直打印导致窗口中全都是文本看的不太舒服,那有没有什么办法可以在每次使用后清屏一下(刷新窗口内容)呢?
可以考虑使用系统命令cls来实现清屏,需要包含头文件
但是呀,如果直接清屏的话,有一些打印的信息就看不到了,能不能按照我们所想,只有按下了某个键后才清屏呢?可以的,比如说按下空格就清屏,用getch(VS下是_getch)函数无回显无缓冲地接收一个字符,此时程序会等待用户键入一个值而暂停下来,输入对应值才继续,进而可以设置一个循环,只有当接收的字符为空格时才跳出循环执行清屏,不然的话就重复接收。
- do
- {
- menu();
- printf("请选择数字:>");
- scanf("%d",&input);
- switch (input)
- {
- case EXIT:
- printf("退出通讯录!");
- break;
- case ADD:
- AddContact(&Con);
- break;
- case DEL:
- DelContact(&Con);
- break;
- case SEARCH:
- SearchContact(&Con);
- break;
- case MODIFY:
- ModifyContact(&Con);
- break;
- case PRINT:
- PrintContact(&Con);
- break;
- case SORT:
- SortContact(&Con);
- break;
- default:
- printf("选择错误,请重新选择!");
- break;
-
- }
-
- char ch;
- do
- {
- printf("按下空格以继续...\n");
- } while ((ch = _getch()) != ' ');
-
- system("cls");
-
- } while (input);
基本上就完成了需求,这就是静态版本的通讯录的实现。
1.通讯录初始化后能存放3个人信息。
2.每当空间放满了后,自动增加2个人信息对应内存空间。

在contact.h中
- //通讯录结构--动态版本
- typedef struct Contact
- {
- struct PeoInfo * data;//指向动态申请的空间,用以存放信息,直接可以当成数组用
- int size;//记录当前通讯录中有效信息个数
- int capacity;//记录当前通讯录中联系人的最大容量
- }Contact;
在contact.h中
- #define DEFAULT_SIZE 3//通讯录初始默认大小
- #define INC_SIZE 2//每次扩容增量
顺便重命名一下结构类型的名称
- //结构类型定义--个人信息
- typedef struct PeoInfo
- {
- char name[MAX_NAME];
- char sex[MAX_SEX];
- int age;
- char tele[MAX_TELE];
- char addr[MAX_ADDR];
- }PeoInfo;
然后再改改初始化函数,在contact.c中
- //初始化通讯录--动态版本
- void InitContact(Contact* pc)
- {
- pc->data = (PeoInfo*)malloc(DEFAULT_SIZE*sizeof(PeoInfo));
- //返回值检查
- if (pc->data == NULL)
- {
- perror("InitContact");
- return;
- }
- pc->size = 0;
- pc->capacity = DEFAULT_SIZE;
- }
其实想想就知道,动态版本较以前版本要改的函数除了初始化以外是不是就只有增加联系人函数了,增加才有可能越界,需要扩容对吧。
在contact.c中
- //增加联系人--动态版本
- void AddContact(Contact* pc)
- {
- //首先判断通讯录是否已满容,考虑增容
- if (pc->size == pc->capacity)
- {
- PeoInfo *ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SIZE) * sizeof(PeoInfo));
- //返回值检查
- if (ptr != NULL)
- {
- pc->data = ptr;
- pc->capacity += INC_SIZE;//容量变量别忘了更新
- printf("联系人已满,成功自动增容!");
- }
-
- else
- {
- perror("AddContact");
- printf("增容失败!\n");
- return;
- }
-
- }
- //增加一个人的信息
- printf("请输入姓名:>");
- scanf("%s", pc->data[pc->size].name);
- getchar();
- printf("请输入年龄:>");
- scanf("%d", &(pc->data[pc->size].age));
- printf("请输入性别:>");
- scanf("%s", pc->data[pc->size].sex);
- getchar();
- printf("请输入电话:>");
- scanf("%s", pc->data[pc->size].tele);
- getchar();
- printf("请输入地址:>");
- scanf("%s", pc->data[pc->size].addr);
- getchar();
-
- pc->size++;//索引+1
- printf("增加成功!\n");
-
- }
因为使用了动态内存,所以最后要释放掉内存,并把指针置为NULL。定义一个销毁通讯录函数。
在contact.c中
- //销毁通讯录
- void DestoryContact(Contact* pc)
- {
- free(pc->data);
- pc->data = NULL;
- }
在test.c中

基本上就完成了需求,这就是动态版本的通讯录的实现。
我们此前写的版本都是将数据存储到内存中的,程序结束后数据自动销毁,具有临时性,那要是我们想要让数据持久化保存该怎么办呢?可以存到硬盘的文件里去,这就要用到文件操作了。
我这里把文本文件contact放在了项目路径下。
这里用wb二进制只写,是因为fwrite函数方便,直接可以把结构输出到文件,反正文件也只是存储数据,不需要打开后能看懂,要查看信息就运行程序把数据读取到内存中查看嘛,所以后面还要实现数据从文件加载到内存中。
- //保存通讯录信息到文件
- void SaveContact(Contact* pc)
- {
- FILE* pf = fopen("contact.txt", "wb");
- //返回值检查
- if (NULL == pf)
- {
- perror("fopen");
- return;
- }
-
- int i = 0;
- for (i = 0; i < pc->size; i++)
- {
- fwrite(pc->data + i, sizeof(PeoInfo), 1, pf);
- }
-
- fclose(pf);
- pf = NULL;
- }
并且是在要退出程序前进行数据输出到文件的。

之前在写动态版本的时候在AddContact函数中写过检查满容并增容的功能,现在把它独立出来封装成一个函数来使用。
- //检查是否满容,若满容会自动增容
- static void CheckCapacity(Contact* pc)
- {
- if (pc->size == pc->capacity)
- {
- PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SIZE) * sizeof(PeoInfo));
- //返回值检查
- if (ptr != NULL)
- {
- pc->data = ptr;
- pc->capacity += INC_SIZE;//容量变量别忘了更新
- printf("联系人已满,成功自动增容!当前容量为:%d\n", pc->capacity);
- }
-
- else
- {
- perror("AddContact");
- printf("增容失败!\n");
- return;
- }
-
- }
- }
什么时候装载呢?当然是初始化通讯录的时候啦。
把文件中存储的信息全部装载到内存中。
设计一个LoadContact函数来实现。
- //初始化通讯录--动态+文件版本
- void InitContact(Contact* pc)
- {
- pc->data = (PeoInfo*)malloc(DEFAULT_SIZE * sizeof(PeoInfo));
- //返回值检查
- if (pc->data == NULL)
- {
- perror("InitContact");
- return;
- }
- pc->size = 0;
- pc->capacity = DEFAULT_SIZE;
-
- LoadContact(pc);//加载文件中已有的信息
-
- }
实际上,一开始我们的通讯录容量十分有限,同时也不知道文件中到底有多少数据,那就不能一股脑地把数据全放到内存去,有可能放不下。
我们得利用动态内存特性,所以CheckCapacity函数就派上用场了。
先把数据一个一个分批次读到PeoInfo类型的临时变量tmp中,判断当前通讯录容量是否足够,若不足自动增容,若足够则将tmp的数据放到通讯录中,用pc->size作为索引(类同于AddContact函数中的思路)。
因为要多次读取嘛,所以用一个while循环,那用什么作为条件呢?fread函数有返回值的,返回的是成功读取对应类型的数据的个数,我们一次读一个,所以拿这个函数返回值和1比较,等于1说明当前读取到一个数据,然后就继续后面代码,最终把数据放入通讯录。
- static void LoadContact(Contact* pc)
- {
- FILE* pf = fopen("contact.txt", "rb");
- //返回值检查
- if (NULL == pf)
- {
- perror("LoadContact");
- return;
- }
-
- PeoInfo tmp = {0};
-
- while (fread(&tmp, sizeof(PeoInfo), 1, pf) == 1)
- {
- CheckCapacity(pc);
- pc->data[pc->size] = tmp;
- pc->size++;
- }
-
- fclose(pf);
- pf = NULL;
-
- }
三个版本全部文件打包:
链接:https://pan.baidu.com/s/1lKF8nq_TpKpA6f1dxsbC_Q?pwd=ylnk
提取码:ylnk
感谢观看,你的鼓励就是对我最大的支持,阁下何不成人之美,点赞收藏关注走一波~