100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > C语言小项目——通讯录的存储系统(静态版 动态版 文件版)

C语言小项目——通讯录的存储系统(静态版 动态版 文件版)

时间:2022-12-12 23:09:03

相关推荐

C语言小项目——通讯录的存储系统(静态版 动态版 文件版)

目录

前言

一、总体设计框架

二、三种通讯录的功能阐述

三、静态通讯录

1、结构体设计

2、初始化通讯录

3、增加联系人的信息

4、删除联系人的信息

5、查找指定联系人并打印

6、修改联系人的信息

7、排序联系人(按姓名)

8、显示所有联系人

9、清空所有联系人

四、动态通讯录

1、结构体重新设计

2、初始化通讯录

3、检查通讯录的容量,判断需不需要扩容

4、清空联系人信息

五、文件版本通讯录

1、读取文件

2、初始化通讯录

3、文件保存

六、通讯录再优化

前言

c语言我们已经学了这么久了,是时候该做一个小项目了,现在这个项目也相当于对前面的知识进行一个总结,我们需要运用到前面学的结构体,动态内存函数,文件操作的相关知识,快来跟我一起看看吧。

一、总体设计框架

与之前写的三字棋与扫雷代码一样,我们把程序封装在三个文件中:

test.c:通讯录的总体逻辑,主要用于对通讯录各功能的测试;

contact.c:通讯录各种功能的具体实现;

contact.h:各种必要的声明,包括库函数头文件的声明、自定义结构的声明以及自定义函数的声明;

二、三种通讯录的功能阐述

我们设计的通讯录主要包含以下版本:

静态版:通讯录内部联系人的增删查改、对联系人按姓名进行排序、显示通讯录中所有联系人、清空通讯录、退出通讯录等功能;

动态版:动态版的通讯录与静态版通讯录功能一样,但是内部实现由固定大小改为动态增容;

文件版:文件版的通讯录与动态版功能与实现方式一样,但是会在程序退出时把联系人信息保存在文件中,在程序运行时把文件中的联系人信息加载到通讯录中;

我们先来阐述最简单的静态通讯录。

三、静态通讯录

静态版的通讯录是最原始的版本,通讯录大小在程序设计时就已经被预设好了,如果存储信息数大于预设值,会提示内存已满,当然可以将预设值设计得非常大,但这极有可能用不完,会造成浪费,抛开容量这个问题来说,静态版通讯录功能还是挺全的,主要运用了自定义类型结构体等知识。

1、结构体设计

这里我们设计两个结构体:一个用于管理联系人的各种属性,比如姓名、年龄、性别、电话号码、住址;另一个结构体用于管理通讯录,它由一个联系人结构体数组加上一个记录联系人数量的变量构成;由于我们这里设计的是静态版的通讯录,所以联系人结构体数组的大小是固定的。

#pragma once //防止头文件重复引用//头文件的包含#include <stdio.h>#include <assert.h> //assert对应头文件#include <string.h> //字符串函数、内存函数对应头文件#include <stdlib.h> //qsort对应头文件//结构的声明#define MAX 100 //联系人的最大数量#define MAX_NAME 20 //宏定义各种联系人信息变量的大小,方便以后修改#define MAX_SEX 10#define MAX_TELE 12#define MAX_ADDR 30//联系人信息结构体typedef struct PeoInfo {char name[MAX_NAME]; //姓名char sex[MAX_SEX]; //性别int age; //年龄char tele[MAX_TELE]; //电话char addr[MAX_ADDR]; //住址}PeoInfo; //结构体重命名//通讯录结构体typedef struct Contact {PeoInfo date[MAX]; //储存联系人信息int count;//记录实际联系人数量}Contact;

2、初始化通讯录

把联系人的信息全都初始化为0就要用到memset函数,memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。

简单介绍一下memset函数

void *memset(void *s, int c, size_t n);

s指向要填充的内存块。c是要被设置的值。n是要被设置该值的字符数。返回类型是一个指向存储区s的指针。

使用该函数可以初始化通讯录:

void InitContact(Contact* pc) //初始化通讯录{assert(pc);memset(pc->date, 0, sizeof(pc->date)); //通讯录里面的信息全部初始化为0pc->count = 0;}

3、增加联系人的信息

这里由于通讯录大小是固定的,所以我们在添加联系人的时候要注意检查通讯录是否已满,如果没满才能正常添加,满了就打印提示信息并直接 return。

void AddPeoInfo(Contact* pc) //添加联系人信息{assert(pc);if (pc->count == MAX) //检查通讯录是否满了{printf("通讯录已满,无法添加联系人\n");return;}else{printf("请输入姓名:>");scanf("%s", pc->date[pc->count].name);printf("请输入性别:>");scanf("%s", pc->date[pc->count].sex);printf("请输入年龄:>");scanf("%d", &(pc->date[pc->count].age));printf("请输入电话:>");scanf("%s", pc->date[pc->count].tele);printf("请输入住址:>");scanf("%s", pc->date[pc->count].addr);pc->count++; //联系人数量++printf("添加联系人成功\n");}}

4、删除联系人的信息

我们先需要检查通讯录是否为空,如果为空提示后直接返回,然后再进行删除操作,先要创建一个find_by_name函数,该函数的作用是检查是否有要删除的联系人,删除信息本质上就是覆盖,找到想要删除的联系人所对应的下标,根据此下标,逐级将后面的结构体数据赋给前面的结构体,这样就完成了删除操作。

static int find_by_name(const Contact* pc, char name[]) //函数用static修饰是为了让该函数只能在本文件内部被使用{assert(pc && name);int i = 0;for (i = 0; i < pc->count; i++){if (strcmp(pc->date[i].name, name) == 0)return i; //找到返回所在位置下标}return -1; //找不到返回-1}

函数返回值的设计应该注意:如果我们查找到了就返回该联系人所在位置的下标,但是如果找不到我们不应该返回0,而是应该返回一个无意义的数,比如-1,因为数组是从0下标开始的,我们所查找的联系人可能在0位置处。

void DeletePeoInfo(Contact* pc)//删除联系人信息{assert(pc);if (pc->count == 0) //通讯录为空直接返回{printf("通讯录为空\n");return;}else{char name[MAX_NAME];printf("请输入要删除的联系人的姓名:>");scanf("%s", &name);int pos = find_by_name(pc, name); //检查通讯录中是否有该联系人if (pos == -1){printf("该联系人不存在\n");}else{int i = 0;for (i = pos; i < pc->count - 1; i++){pc->date[i] = pc->date[i + 1];}pc->count--;printf("删除联系人成功\n");}}}

5、查找指定联系人并打印

和删除联系人一样,我们需要检查通讯录是否为空,调用find函数判断该联系人是否存在。若存在,就打印出来:

void SearchPeoInfo(const Contact* pc) //查找指定联系人信息并打印{assert(pc);if (pc->count == 0){printf("通讯录为空\n");}else{char name[MAX_NAME];printf("请输入要查找的联系人的姓名:>");scanf("%s", name);int pos = find_by_name(pc, name); //检查通讯录中是否有该联系人if (pos == -1){printf("该联系人不存在\n");}else //打印联系人信息{printf("%-20s\t%-10s\t%-5s\t%-12s\t%-30s\n", "姓名", "性别", "年龄", "电话", "住址"); //打印表头printf("%-20s\t%-10s\t%-5d\t%-12s\t%-30s\n", pc->date[pos].name, //打印数据pc->date[pos].sex,pc->date[pos].age,pc->date[pos].tele,pc->date[pos].addr);}}}

6、修改联系人的信息

先要检查通讯录是否为空,再查找联系人是否存在,进而进行修改操作

void ModifyPeoInfo(Contact* pc)//修改联系人信息{assert(pc);if (pc->count == 0){printf("通讯录为空\n");}else{char name[MAX_NAME];printf("请输入要修改的联系人的姓名:>");scanf("%s", name);int pos = find_by_name(pc, name); //检查通讯录中是否有该联系人if (pos == -1){printf("该联系人不存在\n");}else{printf("请输入修改后的姓名:>");scanf("%s", pc->date[pos].name);printf("请输入修改后的性别:>");scanf("%s", pc->date[pos].sex);printf("请输入修改后的年龄:>");scanf("%d", &(pc->date[pos].age));printf("请输入修改后的电话:>");scanf("%s", pc->date[pos].tele);printf("请输入修改后的住址:>");scanf("%s", pc->date[pos].addr);printf("成功修改联系人信息\n");}}}

7、排序联系人(按姓名)

通讯录中的信息存储在一个结构体变量中,普通的排序无法完成任务,因此这里用到了C语言中的库函数 qsort ,它可以适用于所有数据类型的排序,我们来简单介绍一下qsort函数:

qsort介绍:

这是一个库函数,头文件是 stdlib,这个库函数的使用方法在下面,qsort函数可以进行各种数据的排序,无论是整型、字符型还是浮点型,它都能完成排序任务。

关于自定义compar函数的使用:

我们需要告诉qsort函数我们希望数据按照怎么的方式进行比较,比如对于几个字符串,我们可以比较字符串的大小(strcmp),也可以比较字符串的长度(strlen),因此我们要告诉qsort函数我们希望的比较方式,我们就需要传入一个比较函数compar就简写为cmp吧。

来看一个例子吧:

//qsort#include<stdio.h>#include<stdlib.h>int cmp(const void* e1, const void* e2){return *(int*)e1 - *(int*)e2;}int main(){int arr[5] = { 7,3,9,4,6 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp);int i = 0;for(i = 0; i < sz; i++)printf("%d ", arr[i]);return 0;}

int cmp_name(const void* e1, const void* e2) //qsort函数的排序函数{assert(e1 && e2);return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);}void SortContact(Contact* pc) //对通讯录进行排序(按姓名){assert(pc);//qsort的使用:pc->date表示要排序数据的地址,pc->count表示待排序的元素个数,sizeof(PeoInfo)表示一个元素的大小,cmp_name表示排序的方法qsort(pc->date, pc->count, sizeof(PeoInfo), cmp_name);}

我们甚至可以实现年龄等其他规则的排序,甚至可以实现出所有排序方法的函数,然后将其放入函数指针数组中,最后通过回调函数的方式实现任意方式的排序。 大家可以自行试一下。

8、显示所有联系人

void ShowPeoInfo(const Contact* pc)//显示所有联系人信息{assert(pc);if (pc->count == 0){printf("当前通讯录为空\n");}else{int i = 0;printf("%-20s\t%-10s\t%-5s\t%-12s\t%-30s\n", "姓名", "性别", "年龄", "电话", "住址"); //打印表头for (i = 0; i < pc->count; i++){printf("%-20s\t%-10s\t%-5d\t%-12s\t%-30s\n", pc->date[i].name, //打印数据pc->date[i].sex,pc->date[i].age,pc->date[i].tele,pc->date[i].addr);}}}

9、清空所有联系人

直接调用初始化函数即可:

void ClearContact(Contact* pc) //清空所有联系人{assert(pc);InitContact(pc); //清空相当于把通讯录初始化}

这样静态的通讯录就完成了,完整代码在我的gitee,需要的自取。

/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F

四、动态通讯录

动态版解决了静态版最大的痛点——最大容量不好设置,动态版通讯录用到了动态内存管理的知识,遵循用多少、申请多少的原则,动态版通讯录能够无限空间且不会造成浪费,需要注意的是动态开辟的空间,在通讯录结束时要归还给操作系统。

动态版通讯录与静态版通讯录的实现基本相同,只是把固定数组大小变为了可动态增容,相应需要改变的地方函数有:通讯录结构体、初始化函数、增加联系人信息函数(需要判断是否需要增容)、清空联系人信息函数;需要增加的函数有:检查容量函数(容量满了就增容)、销毁通讯录函数。

1、结构体重新设计

存放联系人信息的结构体不变,需要改动的是通讯录结构体,我们之前是在通讯录结构体中定义了一个固定大小的联系人结构体数组,用于存放联系人信息,但是现在我们用将其改为动态增容的,所以我们需要定义一个联系人指针变量,指向动态开辟的一块空间,来存放联系人信息;同时,我们还需要一个capacity变量,来记录当前通讯录的容量,如果当前联系人数量与其相等,我们就增容。

typedef struct PeoInfo {char name[MAX_NAME]; //姓名char sex[MAX_SEX]; //性别int age; //年龄char tele[MAX_TELE]; //电话char addr[MAX_ADDR]; //住址}PeoInfo; //结构体重命名//通讯录结构体typedef struct Contact {PeoInfo *date;//定义一个PeoInfo的指针,指向为其动态开辟的空间int count; //记录实际联系人数量int capacity;//记录当前通讯录得容量,满了就扩容}Contact;

2、初始化通讯录

动态初始化通讯录,我们就要用到calloc函数,在开辟空间的时候同时初始化为0。关于动态内存的知识大家可以看看:动态内存管理。

void InitContact(Contact* pc) //初始化通讯录{assert(pc);pc->date = (PeoInfo*)calloc(DEFAULT_SZ, sizeof(PeoInfo)); //调用calloc函数,开辟空间的同时完成初始化//若果申请失败,打印错误信息后直接返回if (pc->date == NULL){printf("InitContact::%s\n", strerror(errno));return;}//如果申请成功pc->count = 0;pc->capacity = DEFAULT_SZ;}

3、检查通讯录的容量,判断需不需要扩容

关于比较通讯录结构体中的实际联系人数量和当前通讯录容量,如果二者相等,就使用 ralloc 函数对通讯录进行扩容,这里我们一次扩容两倍,这样就可以实现让空间满了,再进行扩容操作,大大节省了空间

static void CheckCapacity(Contact* pc){assert(pc);if (pc->count == pc->capacity){//这里用另一个指针变量ptr接受realloc的返回值,避免开辟失败导致pc的指向丢失PeoInfo* ptr = (PeoInfo*)realloc(pc->date, pc->capacity * CRE_SZ * sizeof(PeoInfo)); //一次扩容CRA_SZ倍//开辟失败if (ptr == NULL){printf("CheckCapacity::%s\n", strerror(errno));return;}//开辟成功pc->date = ptr;pc->capacity *= CRE_SZ;printf("扩容成功\n");}}

4、清空联系人信息

在静态版本,清空联系人信息,我们直接调用初始化函数即可,但是,动态版本清空联系人就不可只这样,直接初始化会导致我们之前申请的空间得不到释放,进而造成内存泄漏,我们可以再创建一个专门销毁通讯录的函数,先释放之前开辟的内存空间。

void DistoryContact(Contact* pc){assert(pc);free(pc->date); //释放date指向的空间pc->date = NULL; //把date置空,避免出现野指针}

void ClearContact(Contact* pc) //清空所有联系人{assert(pc);DistoryContact(pc); //销毁通讯录InitContact(pc);//把通讯录重新初始化printf("成功清空通讯录\n");}

这样动态通讯录也就完成了,完整的代码在我的gittee,需要的可以自取:

/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%8A%A8%E6%80%81%EF%BC%89/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%8A%A8%E6%80%81%EF%BC%89

五、文件版本通讯录

我们发现动态版和静态版本通讯录都存在一个共同的缺陷,那就是我们的联系人信息并没有存储在文件中,都是临时的,每次我们程序重新运行的时候都需要重新添加联系人,这样我们可以使用一个文件版本的通讯录。

文件版通讯录在动态版的基础上增加了两个步骤:一是在销毁通讯录之前把联系人信息存放到 contact.txt 中,避免程序退出后联系人信息丢失;二是在初始化通讯录的时候把 contact.txt 文件中的联系人信息存储到通讯录中;相应需要改变的函数有:初始化通讯录函数;相应需要增加的函数有:加载联系人信息函数、保存联系人信息函数。大家文件相关知识忘记了的话,可以点开:文件相关知识

1、读取文件

读取文件我用了两种方法进行读取,一个是用格式化文件的读取,一个是用二进制文件的读取。

第一种方法:格式化文件读取

//格式化文件读取方式,如果文件重要,即可使用二进制保存即二进制读取void LoadPeoInfo(Contact* pc) //加载联系人信息{assert(pc);FILE* pfRead = fopen("contact.txt", "r"); //以"r"的方式把contact.txt中的信息读到通讯录中if (pfRead == NULL){perror("LoadPeoInfo");return;}while (fscanf(pfRead, "%s %s %d %s %s", pc->date[pc->count].name,pc->date[pc->count].sex,&(pc->date[pc->count].age),pc->date[pc->count].tele,pc->date[pc->count].addr) != EOF){pc->count++;CheckCapacity(pc);}//关闭文件fclose(pfRead);pfRead = NULL;}

第二种方法:二进制文件读取方法

//以二进制方式读取文件,以配套后面二进制进行保存文件void LoadPeoInfo(struct Contact* pc) //加载联系人信息{assert(pc);FILE* pfRead = fopen("contact2.txt", "rb"); //以"rb"的方式把contact.txt中的信息读到通讯录中if (pfRead == NULL){perror("LoadPeoInfo");return;}struct PeoInfo tmp = { 0 };while (fread(&tmp, sizeof(struct PeoInfo), 1, pfRead)){CheckCapacity(pc);pc->date[pc->count] = tmp;pc->count++;}//关闭文件fclose(pfRead);pfRead = NULL;}

2、初始化通讯录

相较于上面的,在初始化函数里面加上一个文件读取函数就行。

void InitContact(Contact* pc) //初始化通讯录{assert(pc);pc->date = (PeoInfo*)calloc(DEFAULT_SZ, sizeof(PeoInfo)); //调用calloc函数,开辟空间的同时完成初始化//若果申请失败,打印错误信息后直接返回if (pc->date == NULL){printf("InitContact::%s\n", strerror(errno));return;}//如果申请成功pc->count = 0;pc->capacity = DEFAULT_SZ;//加载文件中的信息到通讯录中LoadPeoInfo(pc);}

3、文件保存

与上面的读取文件进行配套使用,一个以格式化文件的方式进行保存,一个以二进制文件的方式进行保存。二进制文件保存的好处是打开文件以乱码的形式进行展示,重要信息就可以用二进制进行保存。

格式化进行保存:

//以格式化文件进行保存void SavePeoInfo(const Contact* pc)//保存联系人信息{assert(pc);FILE* pfWrite = fopen("contact.txt", "w"); //以"W"的形式把联系人信息保存到contact.txt中if (pfWrite == NULL){perror("SavePeoInfo");return;}//存储信息int i = 0;for (i = 0; i < pc->count; i++){fprintf(pfWrite, "%-20s\t%-10s\t%-5d\t%-12s\t%-30s\n", pc->date[i].name,pc->date[i].sex,pc->date[i].age,pc->date[i].tele,pc->date[i].addr);}//关闭文件fclose(pfWrite);pfWrite = NULL;}

二进制方式进行保存:

//以二进制文件进行保存void SavePeoInfo(struct Contact* pc)//保存联系人信息{assert(pc);FILE* pfWrite = fopen("contact2.txt", "w"); //以"W"的形式把联系人信息保存到contact.txt中if (pfWrite == NULL){perror("SavePeoInfo");return;}//存储信息int i = 0;for (i = 0; i < pc->count; i++){fwrite(pc->date + i, sizeof(struct PeoInfo), 1, pfWrite);}//关闭文件fclose(pfWrite);pfWrite = NULL;}

这样通讯录文件版也就写完了,需要的在我的gittee自取:

/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F(%E6%96%87%E4%BB%B6%EF%BC%89/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F(%E6%96%87%E4%BB%B6%EF%BC%89

六、通讯录再优化

我用回调函数的方法实现了通讯录选择哪种排序的功能。

void option(){printf("**************************\n");printf("****1.name 2.age****\n");printf("****3.sex 4.tele***\n");printf("****5.addr0.exit****\n");printf("**************************\n");}

int cmp_name(const void* e1, const void* e2) //qsort函数的排序函数按姓名{assert(e1 && e2);return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);}int cmp_age(const void* e1, const void* e2) //qsort函数的排序函数按年龄{assert(e1 && e2);return ((struct PeoInfo*)e1)->age - ((struct PeoInfo*)e2)->age;}int cmp_sex(const void* e1, const void* e2){return strcmp(((struct PeoInfo*)e1)->sex, ((struct PeoInfo*)e2)->sex);}int cmp_tele(const void* e1, const void* e2){return strcmp(((struct PeoInfo*)e1)->tele, ((struct PeoInfo*)e2)->tele);}int cmp_addr(const void* e1, const void* e2){return strcmp(((struct PeoInfo*)e1)->addr, ((struct PeoInfo*)e2)->addr);}void SortPeoInfo(struct Contact* pc){int intput=-1;do{option();printf("请输入排序方式的选项:\n");scanf("%d", &intput);switch (intput){case 0:printf("退出修改成功!\n");break;case 1:qsort(pc->date, pc->count, sizeof(pc->date[0]), cmp_name);ShowPeoInfo(pc);break;case 2:qsort(pc->date, pc->count, sizeof(pc->date[0]), cmp_age);ShowPeoInfo(pc);break;case 3:qsort(pc->date, pc->count, sizeof(pc->date[0]), cmp_sex);ShowPeoInfo(pc);break;case 4:qsort(pc->date, pc->count, sizeof(pc->date[0]), cmp_tele);ShowPeoInfo(pc);break;case 5:qsort(pc->date, pc->count, sizeof(pc->date[0]), cmp_addr);ShowPeoInfo(pc);break;default:printf("输入错误,请重新输入:\n");}} while (intput);return;}

通讯录优化的代码也发到了gittee,大家可以自行去看:

/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E5%86%8D%E4%BC%98%E5%8C%96/code-chen-shuai/code-written-in-/tree/master/%E9%80%9A%E8%AE%AF%E5%BD%95%E7%AE%A1%E7%90%86%E5%86%8D%E4%BC%98%E5%8C%96

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。