C语言实现通讯录管理系统—动态内存分配型并保存信息、

目录

一、通讯录功能需求分析:

二、文件架构分析:

三、代码,分模块呈现:

3.1 主函数(main函数)部分:

3.2 menu菜单函数部分:

3.3 枚举类型的声明部分:

3.4 初始化通讯录部分:

3.5 增加通讯录联系人信息部分:

3.6 显示联系人信息的部分:

3.7 删除联系人信息的部分:

扫描二维码关注公众号,回复: 14288711 查看本文章

3.8 查找联系人信息的部分:

3.9 修改联系人信息的部分:

3.10 排序联系人信息的部分:

3.11 清空所有联系人信息的部分:

3.12 销毁通讯录的部分:

3.13 保存通讯录的信息到文件的部分:

四、代码以文件进行呈现:

4.1 test.c源文件

        4.2 contact.c源文件

        4.3 contact.h头文件


一、通讯录功能需求分析:

为了实现通讯录管理系统,为此,要保证实现以下的功能:

通讯录最开始初始化后能存放3个人的信息,当空间存放满的时候,再增加2个联系人信息、如果再发现不够使用,每次增加2个联系人的信

息、、每个人的信息包含:名字、年龄、性别、电话、地址、除此之外,还是实现:增加人的信息、删除人的信息、修改指定人的信息、查

找指定人的信息、清空联系人的信息、显示联系人的信息、排序通讯录的信息、销毁通讯录。

能否把联系人的信息写到文件中去呢?当通讯录退出的时候,把联系人的信息都写到文件中去,当通讯录初始化的时候,加载文件中的信

息到通讯录中去。

二、文件架构分析:

1、test.c  —  用来测试通讯录的模块、主函数接口引入。

2、contact.h — 关于通讯录相关的 类型的定义、函数的声明、库函数的头文件等、

3、contact.c — 函数的实现、

写完contact.h、contact.c 之后,将两者引用到 test.c 中使用即可满足要求。

三、代码,分模块呈现:

3.1 主函数(main函数)部分:

int main()
{
	int input = 0;
	//创建通讯录,结构体类型在头文件中定义的,所以要引头文件,
	通讯录中当前有几个元素:
	//int sz = 0;
	//创建通讯录
	Contact con; //con就是通讯录,也可以直接进行初始化,但是为了更好的体现模块化,就对初始化通讯录封装一个函数。
	//如果想把其中的一部分初始化为0,就必须使用函数来做了。

	//初始化通讯录
	//使用malloc函数在堆区上动态开辟一块连续的内存空间,并把该空间的起始位置的地址放在结构体指针变量data中。
	//初始化整型变量sz=0;
	//初始化整型变量capacity为当前最大的容量、
	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 SORT:
				SortContact(&con);
				break;
				//显示所有联系人的信息
			case PRINT:
				//虽然只是打印信息,不会改变实参的信息,但是考虑的效率的话,还是使用传址调用比较好,结构体传参最好传地址。
				PrintContact(&con);
				break;
				//清空所有联系人的信息
			case CLEAR:
				ClearContact(&con);
				break;
			case EXIT:
				//选择退出通讯录,应该先把联系人的信息保存在文件中去之后,然后再进行销毁通讯录、
				SaveContatct(&con);

				//现在存放联系人信息的内存空间是动态开辟的,当退出的时候,该空间要被释放掉才行、
				//销毁通讯录
				DestoryContact(&con);
				printf("退出通讯录\n");
				break;
			default:
				printf("选择错误,请重新进行选择:>");
				break;
		}
	} while (input);
	//如果有同名的,一律操作第一个出现该名字的那个成员,因为遍历是从前往后遍历的,在这里不考虑同名的情况。
	return 0;
}

3.2 menu菜单函数部分:

void menu()
{
	printf("*********************************\n");
	printf("******  1、add     2、del  ******\n");
	printf("******  3、search  4、modify ****\n");
	printf("******  5、sort    6、print******\n");
	printf("******  7、clear   0、exit  *****\n");
	printf("*********************************\n");
}

在这里注意一下各种功能的编号switch中的case的选择对应起来。

3.3 枚举类型的声明部分:

enum Option//枚举成员变量从0开始,依次递增1;
{
	//枚举中的变量一般都采用大写。
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	PRINT,
	CLEAR
};

枚举类型的定义,要注意的是,枚举成员变量按照顺序默认从0开始,依次递增1也可给枚举成员变量设置初值、

3.4 初始化通讯录部分:


//如果直接把该函数的定义放在初始化通讯录函数的前面, 则就不需要声明该函数,如果把该函数的定义放在了初始化通讯录的后面的话
//并且再不进行声明的话,就会报错说该函数未定义,所以,在这里有两种方法,要么就不声明该函数,把该函数放在初始化通讯录函数的
//前面,,,要么就声明该函数,这样的话,只需要在该contact.c中包含一下头文件,既可以把该函数放在该源文件的任何位置。
void LoadContact(Contact* pc)//一级指针传参,一级指针接收、
{
	//读文件
	FILE* pf = fopen("contact.dat", "r");
	{
		if (pf == NULL)
		{
			//打开文件失败
			perror(" LoadContact");
			return;
		}
		else
		{
			//打开文件成功
			//假设上一次运行通讯录添加的人数为100个,退出通讯录的时候,会把这100个联系人的信息存储在文件中,等下一次运行通讯录的时候,读取文件时
			//会把这100个联系人的信息先添加到本次的数组中,,但是,由于可以动态开辟内存空间,所以每一次运行通讯录的时候,设置的最多存储联系人的个数不是很大, 
			//如果设置的最多存储联系人的个数为3个,但是现在有100个联系人的信息要添加进去,放不开,所以要先进行检查本次读取文件中的信息是否能够在本次运行
			//通讯录时能否放得开,如果放不开就要先扩容,再往里放联系人的信息。
			
			//fread函数的返回值代表的是,本次读取中,实际读取到的完整的元素的个数,该函数的第三个参数代表的是,每一次使用该函数最多能够读取到的元素的个数,
			//假设第三个参数为5,即,一次最多读取5个元素,如果能够读取到5个元素,那么该函数的返回值就是5,返回的是该函数真实读取到的元素的个数,
			//如果某一个文件中只放了8个元素,使用fread函数去读取,设第三个参数为5,第一次能够读取到5个元素,返回值是5,但是第二次只能读取到3个元素,所以
			//返回值是3,,该函数的第三个参数代表的是最多能够读取到的元素的个数,但不一定能读取到,而该函数的返回值则是真实读取到的元素的个数。、

			PeoInfo tmp = { 0 };//不完全初始化。
			while (fread(&tmp, sizeof(PeoInfo), 1, pf))//当返回值为0的时候,代表文件中的联系人的信息都读取完了、此时就不再进循环中了、
			{
				//检测容量,不够则进行扩容
				Check_Capacity(pc);
				pc->data[pc->sz] = tmp;
				pc->sz++;
			}
		}
		//关闭文件
		fclose(pf);
		pf = NULL;
	}
}
//初始化 通讯录函数
void InitContact(Contact* pc)
{
	pc->data = (PeoInfo*)malloc(DEFAULT_SZ * sizeof(PeoInfo));
	if (pc->data == NULL)
	{
		//开辟内存空间失败
		perror("InitContact"); //由于在函数InitContact中进行的操作,如果开辟失败,报错是该函数足够的空间、
		return;  //由于InitContact函数的返回类型是void类型,所以直接返回 return,即可。
	}
	//在这里对结构体指针变量data指向的内存空间中的内容不进行初始化,可以初始化,也可以不初始化;

	(pc->sz) = 0;//初始化后默认为0、
	(pc->capacity) = DEFAULT_SZ;//初始化后默认为DEFAULT_SZ个、

	//加载文件——读文件
	LoadContact(pc);//一级指针传参
}

3.5 增加通讯录联系人信息部分:


void Check_Capacity(Contact* pc)
{
	if (pc->sz == pc->capacity)
	{
		//增容
		PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SZ)*sizeof(PeoInfo));
		if (ptr == NULL)
		{
			//增容失败
			perror("AddContact");
			printf("增容联系人失败\n");
			return;
		}
		else
		{
			//增容成功 
			pc->data = ptr;
			pc->capacity += INC_SZ;
			printf("增容成功\n");
		}
	}
}
//增加联系人的信息 —— 动态
void AddContact(Contact* pc)
{

	//通讯录满员-> 增容
	Check_Capacity(pc);//一级指针传参,一级指针接收。

	//通讯录未满,可以添加新成员,增加一个人的信息
	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].addr);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->sz].tele);
	(pc->sz)++;
	printf("增加联系人员成功\n");
	//在这里,虽然[ ]的优先级高于->,,但是,data和[ ]是不可以先进行结合的,因为,这是在一个调用函数中,形参那部分接受到的只有指针变量pc
	//也就是说,如果后两者进行结合的话,系统根本就不知道data是什么东西,所以它结合出来是错误的,即,虽然[ ]的优先级高于->,但是后两者不能
	//进行结合会出错,所以,即使[]的优先级比较高,还是先让data和->进行结合,即先进行pc->data的操作,在结构体成员变量中找到了指针变量data,
	//又因为该指针变量data指向了由malloc函数在堆区上动态开辟的内存空间的起始位置的地址,相当于数组首元素的地址,在这里不是特例,所以,
	//就等价于找到了该数组的数组名,即,pc->data === 数组名, 知道了数组名,再通过数组下标进行访问数组中的元素。
}

3.6 显示联系人信息的部分:


//显示联系人的信息
void PrintContact(const Contact* pc)
{
	//打印出所有人的信息,即sz个人的信息。
	int i = 0;
	//打印标题
	printf("%-10s\t%-5s\t%-5s\t%-12s\t%-50s\n", "名字", "年龄", "性别", "电话", "地址");// \t === tab
	//打印数据
	for (i = 0; i < (pc->sz); i++)
	{
		printf("%-10s\t%-5d\t%-5s\t%-12s\t%-50s\n", pc->data[i].name, 
													pc->data[i].age,
													pc->data[i].sex,
													pc->data[i].tele,
													pc->data[i].addr);
	}
	//先进行pc->和data的结合,找到了结构体中的指针变量data,而该指针变量中存放的是由malloc函数在堆区上动态开辟的内存空间的起始位置的地址,相当于数组首元素的地址,
	//在这里不是特例,所以,就等价于找到了该数组的数组名,即,pc->data === 数组名,, 知道了数组名,再通过数组下标进行访问数组中的元素,找到了了数组下标为i的元素
	//即找到了一个变量,再通过点来访问该人的姓名等,,由于姓名是一个数组,找到的就是整个数组,整个数组又可以使用数组名来表示,即:pc->data[i].name === name ,没有sizeof和&,
	//代表数组首元素的地址,然后再以%s进行打印,除了年龄是一个变量,其他的都是数组,和name同理。

3.7 删除联系人信息的部分:

 
//因为该函数只是为了满足删除,查找,修改功能的需要,而这三个功能对应的函数的实现都会在该 .c 文件内进行实现,所以,对于这个函数
//只需要在该 .c 文件内执行即可,不许要暴露给别人,,所以,在前面加上static,就固定了该函数只在目前所在的 .c 文件内进行工作即可。
//static 修饰函数,本质上是改变了函数的链接属性。
static int Find_By_Name(const Contact* pc,char name[])//数组形式接收,数组形式接受的话就不考虑const的使用了。
{
	int i = 0;
	for (i = 0; i < pc -> sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)//相等
	//第一个参数先找到整个数组,可以使用数组名来表示,不是特例,即代表数组首元素的地址,第二个参数也不是特例,也是数组首元素的地址。
		{
			return i;
		}
	}
	return -1;
}
//删除联系人信息
void DelContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	if (pc->sz == 0)
	{
		printf("通讯录为空、不可以再进行删除操作\n");
		return;
	}
	//删除某个人的信息
	printf("请输入要删除人员的姓名:>");
	scanf("%s", name);
	//1、查找要删除的人
	//不管是删除还是查找还是修改,都需要使用到查找这个功能,所以就单独把该功能拿出来封装一个函数;
	int pos=Find_By_Name(pc,name);//一级指针传参和数组名传参
	//不存在该人
	if (pos == -1)
	{
		printf("要删除的人员不存在\n");
		return;
	}
	//2、存在该人员,要进行删除,把数组该位置上的人员删除之后,数组后面的人员依次往前移动一个位置。
	int i = 0;
	for (i = pos; i < (pc->sz - 1); i++)
	{
		pc->data[i] = pc->data[i + 1]; 
	}
	//pc->sz -= 1;
	pc->sz--;
	//如果想删除最后一个,是删除不掉的,因为,如果10个元素,最后一个下标为9,判断条件是<9,,所以不进入循环,但是
	//循环后面还有pc->sz--,,成员个数少了1,再显示人员信息的时候,访问不到最后一个人员了,,即使没删掉,也访问不掉,
	//最后的结果和删掉最后一个人员的效果是一样的。
	//假设MAX=3,把最后一个元素删除,本质上并没有从数组中删除掉,而是因为sz减1,打印的时候不访问最后一个元素,看起来和删除的效果是一样的,现在由于
	//看起来删了,本质上没删去,如果再添加新元素会怎么样呢?
	//因为删除完之后,元素个数就会减去1,由原来的3变成了2,,再添加新元素的时候,就会直接把新元素的内容放在下标为2的位置上,这样的话,即使之前的最后一个元素
	//没删去,也会被新的元素覆盖掉,添加完之后元素个数加1,再打印出来就是添加成员后的信息,是对的。
	printf("删除联系人员成功\n");
}

3.8 查找联系人信息的部分:

//查找联系人信息
void SearchContact(const Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	//查找某个人的信息
	printf("请输入要查找人员的姓名:>");
	scanf("%s", name);
	int pos = Find_By_Name(pc, name);
	//要查找的人员不存在
	if (pos == -1)
	{
		printf("要查找的人员不存在\n");
		return;
	}
	else
	{
		//2、存在该人员,找出之后并打印出该成员的信息
		//打印标题
		printf("%-10s\t%-5s\t%-5s\t%-12s\t%-50s\n", "名字", "年龄", "性别", "电话", "地址");// \t === tab
		//打印数据
		printf("%-10s\t%-5d\t%-5s\t%-12s\t%-50s\n", 
			pc->data[pos].name,
			pc->data[pos].age,
			pc->data[pos].sex,
			pc->data[pos].tele,
			pc->data[pos].addr);
	}
}

3.9 修改联系人信息的部分:

//修改指定联系人的信息
void ModifyContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	//修改某个人的信息
	printf("请输入要修改人员的姓名:>");
	scanf("%s", name);
	int pos = Find_By_Name(pc, name);
	//要修改的人员不存在
	if (pos == -1)
	{
		printf("要修改的人员不存在\n");
		return;
	}
	else
	{
		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].addr);
		printf("请输入修改后人员的电话:>");
		scanf("%s", pc->data[pos].tele);
		printf("修改联系人员信息成功\n");
	}
}

3.10 排序联系人信息的部分:


enum Option_qsort
{
	exit_qsort,
	name,
	age,
	sex,
	addr,
	tele
};
void menu_qsort()
{
	printf("*****************************\n");
	printf("***** 1、name   2、age  *****\n");
	printf("***** 3、sex    4、addr *****\n");
	printf("***** 5、tele   0、exit_sort*\n");
	printf("*****************************\n");
}


//按照名字进行排序
int Conpare_ByName(const void*e1, const void* e2)
{
	return strcmp( ((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name );
}

//按照年龄进行排序
int Conpare_ByAge(const void*e1, const void* e2)
{
	return ((PeoInfo*)e1)->age - ((PeoInfo*)e2)->age;
}

//按照性别进行排序
int Conpare_BySex(const void*e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->sex, ((PeoInfo*)e2)->sex);
}

//按照地址进行排序
int Conpare_ByAddr(const void*e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->addr, ((PeoInfo*)e2)->addr);
}

//按照电话进行排序
int Conpare_ByTele(const void*e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->tele, ((PeoInfo*)e2)->tele);
}

void print(Contact* pc)
{
	int i = 0;
	printf("%-10s\t%-5s\t%-5s\t%-12s\t%-50s\n", "名字", "年龄", "性别", "电话", "地址");

	for (i = 0; i < pc->sz; i++)
	{
		printf("%-10s\t%-5d\t%-5s\t%-12s\t%-50s\n", pc->data[i].name,
			pc->data[i].age,
			pc->data[i].sex,
			pc->data[i].tele,
			pc->data[i].addr);
	}
}
//排序联系人的信息
void SortContact(Contact* pc)
{
	int input = 0;
	do{
		menu_qsort();
		printf("请选择排序的方式:>");
		scanf("%d", &input);
		switch (input)
		{
			//按照名字进行排序
		case name:
			//这里求数组元素个数的时候,不要使用整个数组的大小比上数组首元素的大小,因为我们在这里排序的时候,仅对已添加的成员进行排序,比如添加的成员个数是3的话,
			//就对这3个元素进行排序,如果开辟的数组有1000个元素,但是只添加3个的话,如果使用整个数组的大小比上数组首元素的大小,求出来的就是1000,意思是对1000个元素进行排序
			//这样是不对的,应该对添加进去的3个元素进行排序,pc->sz,这个整体就已经代表了元素个数是3,不需要再计算了,所以,使用下面的代码即可。
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByName);
			print(pc);
			break;
			//按照年龄进行排序
		case age:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByAge);
			print(pc);
			break;
			//按照性别进行排序
		case sex:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_BySex);
			print(pc);
			break;
			//按照地址进行排序
		case addr:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByAddr);
			print(pc);
			break;
			//按照电话进行排序
		case tele:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByTele);
			print(pc);
			break;
			//退出排序
		case exit_qsort:
			printf("退出排序\n");
			printf("\n");
			break;
			//选择错误
		default:
			printf("选择排列方式错误,请重新进行选择:>\n");
			break;
		}
	}while (input);
}

要知道库函数qsort的使用规则,上述代码可以实现多次排序,并且可以按照不同的要求进行排序,要注意的是,不同类型的内容进行排序

的时候,两个元素的比较方式不同,要写出不同的代码。

3.11 清空所有联系人信息的部分:


//清空所有联系人的信息
void ClearContact(Contact* pc)
{
	(pc->sz) = 0;
	memset(pc->data, 0, sizeof(pc->data));
	printf("清空成功\n");
	printf("\n");
	//在此过程把sz置为了0,再次打印的时候,不进入遍历的循环,不打印数据,直接出程序。
}


使用memset内存设置函数,并把数组元素个数设为0,值得注意的是:在此过程把sz置为了0,再次打印的时候,不进入遍历的循环,不打

印数据,直接出程序,所以效果和清空所有联系人的效果一样、

3.12 销毁通讯录的部分:

//销毁通讯录
void DestoryContact(Contact* pc)
{
	//释放
	free(pc->data);
	pc->data = NULL;
	pc->sz = 0;
	pc->capacity = 0;
}

3.13 保存通讯录的信息到文件的部分:

//保存通讯录的信息到文件
void SaveContatct(const Contact* pc)
{
	//写文件
	FILE* pf = fopen("contact.dat", "w");
	if (pf == NULL)
	{
		//打开文件失败
		perror("SaveContatct");
		return ;
	}
	else
	{
		//打开文件成功,写入文件,以二进制的形式写入文件、
		int i = 0;
		for (i = 0; i < pc->sz; i++)
		{
			fwrite(pc->data + i, sizeof(PeoInfo), 1, pf);
		}
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
}

四、代码以文件进行呈现:

4.1 test.c源文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

//能否把联系人的信息写到文件中去呢?
//当通讯录退出的时候,把联系人的信息都写到文件中去
//当通讯录初始化的时候,加载文件中的信息到通讯录中去。

//通讯录
//每个人的信息包含:名字、年龄、性别、电话、地址、
//增加人的信息、删除人的信息、修改指定人的信息、查找指定人的信息、排序通讯录的信息、清除所有信息、显示联系人信息,销毁通讯录、

//动态增长版本
//1、通讯录最开始初始化后能存放3个人的信息,
//2、当空间存放满的时候,再增加2个联系人信息、
//3、如果再发现不够使用,每次增加2个联系人的信息、

enum Option//枚举成员变量从0开始,依次递增1;
{
	//枚举中的变量一般都采用大写。
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	PRINT,
	CLEAR
};

void menu()
{
	printf("*********************************\n");
	printf("******  1、add     2、del  ******\n");
	printf("******  3、search  4、modify ****\n");
	printf("******  5、sort    6、print******\n");
	printf("******  7、clear   0、exit  *****\n");
	printf("*********************************\n");
}



int main()
{
	int input = 0;
	//创建通讯录,结构体类型在头文件中定义的,所以要引头文件,
	通讯录中当前有几个元素:
	//int sz = 0;
	//创建通讯录
	Contact con; //con就是通讯录,也可以直接进行初始化,但是为了更好的体现模块化,就对初始化通讯录封装一个函数。
	//如果想把其中的一部分初始化为0,就必须使用函数来做了。

	//初始化通讯录
	//使用malloc函数在堆区上动态开辟一块连续的内存空间,并把该空间的起始位置的地址放在结构体指针变量data中。
	//初始化整型变量sz=0;
	//初始化整型变量capacity为当前最大的容量、
	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 SORT:
				SortContact(&con);
				break;
				//显示所有联系人的信息
			case PRINT:
				//虽然只是打印信息,不会改变实参的信息,但是考虑的效率的话,还是使用传址调用比较好,结构体传参最好传地址。
				PrintContact(&con);
				break;
				//清空所有联系人的信息
			case CLEAR:
				ClearContact(&con);
				break;
			case EXIT:
				//选择退出通讯录,应该先把联系人的信息保存在文件中去之后,然后再进行销毁通讯录、
				SaveContatct(&con);

				//现在存放联系人信息的内存空间是动态开辟的,当退出的时候,该空间要被释放掉才行、
				//销毁通讯录
				DestoryContact(&con);
				printf("退出通讯录\n");
				break;
			default:
				printf("选择错误,请重新进行选择:>");
				break;
		}
	} while (input);
	//如果有同名的,一律操作第一个出现该名字的那个成员,因为遍历是从前往后遍历的,在这里不考虑同名的情况。
	return 0;
}

4.2 contact.c源文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"



//如果直接把该函数的定义放在初始化通讯录函数的前面, 则就不需要声明该函数,如果把该函数的定义放在了初始化通讯录的后面的话
//并且再不进行声明的话,就会报错说该函数未定义,所以,在这里有两种方法,要么就不声明该函数,把该函数放在初始化通讯录函数的
//前面,,,要么就声明该函数,这样的话,只需要在该contact.c中包含一下头文件,既可以把该函数放在该源文件的任何位置。
void LoadContact(Contact* pc)//一级指针传参,一级指针接收、
{
	//读文件
	FILE* pf = fopen("contact.dat", "r");
	{
		if (pf == NULL)
		{
			//打开文件失败
			perror(" LoadContact");
			return;
		}
		else
		{
			//打开文件成功
			//假设上一次运行通讯录添加的人数为100个,退出通讯录的时候,会把这100个联系人的信息存储在文件中,等下一次运行通讯录的时候,读取文件时
			//会把这100个联系人的信息先添加到本次的数组中,,但是,由于可以动态开辟内存空间,所以每一次运行通讯录的时候,设置的最多存储联系人的个数不是很大, 
			//如果设置的最多存储联系人的个数为3个,但是现在有100个联系人的信息要添加进去,放不开,所以要先进行检查本次读取文件中的信息是否能够在本次运行
			//通讯录时能否放得开,如果放不开就要先扩容,再往里放联系人的信息。
			
			//fread函数的返回值代表的是,本次读取中,实际读取到的完整的元素的个数,该函数的第三个参数代表的是,每一次使用该函数最多能够读取到的元素的个数,
			//假设第三个参数为5,即,一次最多读取5个元素,如果能够读取到5个元素,那么该函数的返回值就是5,返回的是该函数真实读取到的元素的个数,
			//如果某一个文件中只放了8个元素,使用fread函数去读取,设第三个参数为5,第一次能够读取到5个元素,返回值是5,但是第二次只能读取到3个元素,所以
			//返回值是3,,该函数的第三个参数代表的是最多能够读取到的元素的个数,但不一定能读取到,而该函数的返回值则是真实读取到的元素的个数。、

			PeoInfo tmp = { 0 };//不完全初始化。
			while (fread(&tmp, sizeof(PeoInfo), 1, pf))//当返回值为0的时候,代表文件中的联系人的信息都读取完了、此时就不再进循环中了、
			{
				//检测容量,不够则进行扩容
				Check_Capacity(pc);
				pc->data[pc->sz] = tmp;
				pc->sz++;
			}
		}
		//关闭文件
		fclose(pf);
		pf = NULL;
	}
}
//初始化 通讯录函数
void InitContact(Contact* pc)
{
	pc->data = (PeoInfo*)malloc(DEFAULT_SZ * sizeof(PeoInfo));
	if (pc->data == NULL)
	{
		//开辟内存空间失败
		perror("InitContact"); //由于在函数InitContact中进行的操作,如果开辟失败,报错是该函数足够的空间、
		return;  //由于InitContact函数的返回类型是void类型,所以直接返回 return,即可。
	}
	//在这里对结构体指针变量data指向的内存空间中的内容不进行初始化,可以初始化,也可以不初始化;

	(pc->sz) = 0;//初始化后默认为0、
	(pc->capacity) = DEFAULT_SZ;//初始化后默认为DEFAULT_SZ个、

	//加载文件——读文件
	LoadContact(pc);//一级指针传参
}



void Check_Capacity(Contact* pc)
{
	if (pc->sz == pc->capacity)
	{
		//增容
		PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SZ)*sizeof(PeoInfo));
		if (ptr == NULL)
		{
			//增容失败
			perror("AddContact");
			perror("增容联系人\n");
			return;
		}
		else
		{
			//增容成功 
			pc->data = ptr;
			pc->capacity += INC_SZ;
			printf("增容成功\n");
		}
	}
}
//增加联系人的信息 —— 动态
void AddContact(Contact* pc)
{

	//通讯录满员-> 增容
	Check_Capacity(pc);//一级指针传参,一级指针接收。

	//通讯录未满,可以添加新成员,增加一个人的信息
	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].addr);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->sz].tele);
	(pc->sz)++;
	printf("增加联系人员成功\n");
	//在这里,虽然[ ]的优先级高于->,,但是,data和[ ]是不可以先进行结合的,因为,这是在一个调用函数中,形参那部分接受到的只有指针变量pc
	//也就是说,如果后两者进行结合的话,系统根本就不知道data是什么东西,所以它结合出来是错误的,即,虽然[ ]的优先级高于->,但是后两者不能
	//进行结合会出错,所以,即使[]的优先级比较高,还是先让data和->进行结合,即先进行pc->data的操作,在结构体成员变量中找到了指针变量data,
	//又因为该指针变量data指向了由malloc函数在堆区上动态开辟的内存空间的起始位置的地址,相当于数组首元素的地址,在这里不是特例,所以,
	//就等价于找到了该数组的数组名,即,pc->data === 数组名, 知道了数组名,再通过数组下标进行访问数组中的元素。
}



//显示联系人的信息
void PrintContact(const Contact* pc)
{
	//打印出所有人的信息,即sz个人的信息。
	int i = 0;
	//打印标题
	printf("%-10s\t%-5s\t%-5s\t%-12s\t%-50s\n", "名字", "年龄", "性别", "电话", "地址");// \t === tab
	//打印数据
	for (i = 0; i < (pc->sz); i++)
	{
		printf("%-10s\t%-5d\t%-5s\t%-12s\t%-50s\n", pc->data[i].name, 
													pc->data[i].age,
													pc->data[i].sex,
													pc->data[i].tele,
													pc->data[i].addr);
	}
	//先进行pc->和data的结合,找到了结构体中的指针变量data,而该指针变量中存放的是由malloc函数在堆区上动态开辟的内存空间的起始位置的地址,相当于数组首元素的地址,
	//在这里不是特例,所以,就等价于找到了该数组的数组名,即,pc->data === 数组名,, 知道了数组名,再通过数组下标进行访问数组中的元素,找到了了数组下标为i的元素
	//即找到了一个变量,再通过点来访问该人的姓名等,,由于姓名是一个数组,找到的就是整个数组,整个数组又可以使用数组名来表示,即:pc->data[i].name === name ,没有sizeof和&,
	//代表数组首元素的地址,然后再以%s进行打印,除了年龄是一个变量,其他的都是数组,和name同理。
}


 
//因为该函数只是为了满足删除,查找,修改功能的需要,而这三个功能对应的函数的实现都会在该 .c 文件内进行实现,所以,对于这个函数
//只需要在该 .c 文件内执行即可,不许要暴露给别人,,所以,在前面加上static,就固定了该函数只在目前所在的 .c 文件内进行工作即可。
//static 修饰函数,本质上是改变了函数的链接属性。
static int Find_By_Name(const Contact* pc,char name[])//数组形式接收,数组形式接受的话就不考虑const的使用了。
{
	int i = 0;
	for (i = 0; i < pc -> sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)//相等
	//第一个参数先找到整个数组,可以使用数组名来表示,不是特例,即代表数组首元素的地址,第二个参数也不是特例,也是数组首元素的地址。
		{
			return i;
		}
	}
	return -1;
}
//删除联系人信息
void DelContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	if (pc->sz == 0)
	{
		printf("通讯录为空、不可以再进行删除操作\n");
		return;
	}
	//删除某个人的信息
	printf("请输入要删除人员的姓名:>");
	scanf("%s", name);
	//1、查找要删除的人
	//不管是删除还是查找还是修改,都需要使用到查找这个功能,所以就单独把该功能拿出来封装一个函数;
	int pos=Find_By_Name(pc,name);//一级指针传参和数组名传参
	//不存在该人
	if (pos == -1)
	{
		printf("要删除的人员不存在\n");
		return;
	}
	//2、存在该人员,要进行删除,把数组该位置上的人员删除之后,数组后面的人员依次往前移动一个位置。
	int i = 0;
	for (i = pos; i < (pc->sz - 1); i++)
	{
		pc->data[i] = pc->data[i + 1]; 
	}
	//pc->sz -= 1;
	pc->sz--;
	//如果想删除最后一个,是删除不掉的,因为,如果10个元素,最后一个下标为9,判断条件是<9,,所以不进入循环,但是
	//循环后面还有pc->sz--,,成员个数少了1,再显示人员信息的时候,访问不到最后一个人员了,,即使没删掉,也访问不掉,
	//最后的结果和删掉最后一个人员的效果是一样的。
	//假设MAX=3,把最后一个元素删除,本质上并没有从数组中删除掉,而是因为sz减1,打印的时候不访问最后一个元素,看起来和删除的效果是一样的,现在由于
	//看起来删了,本质上没删去,如果再添加新元素会怎么样呢?
	//因为删除完之后,元素个数就会减去1,由原来的3变成了2,,再添加新元素的时候,就会直接把新元素的内容放在下标为2的位置上,这样的话,即使之前的最后一个元素
	//没删去,也会被新的元素覆盖掉,添加完之后元素个数加1,再打印出来就是添加成员后的信息,是对的。
	printf("删除联系人员成功\n");
}




//查找联系人信息
void SearchContact(const Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	//查找某个人的信息
	printf("请输入要查找人员的姓名:>");
	scanf("%s", name);
	int pos = Find_By_Name(pc, name);
	//要查找的人员不存在
	if (pos == -1)
	{
		printf("要查找的人员不存在\n");
		return;
	}
	else
	{
		//2、存在该人员,找出之后并打印出该成员的信息
		//打印标题
		printf("%-10s\t%-5s\t%-5s\t%-12s\t%-50s\n", "名字", "年龄", "性别", "电话", "地址");// \t === tab
		//打印数据
		printf("%-10s\t%-5d\t%-5s\t%-12s\t%-50s\n", 
			pc->data[pos].name,
			pc->data[pos].age,
			pc->data[pos].sex,
			pc->data[pos].tele,
			pc->data[pos].addr);
	}
}

//修改指定联系人的信息
void ModifyContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	//修改某个人的信息
	printf("请输入要修改人员的姓名:>");
	scanf("%s", name);
	int pos = Find_By_Name(pc, name);
	//要修改的人员不存在
	if (pos == -1)
	{
		printf("要修改的人员不存在\n");
		return;
	}
	else
	{
		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].addr);
		printf("请输入修改后人员的电话:>");
		scanf("%s", pc->data[pos].tele);
		printf("修改联系人员信息成功\n");
	}
}


enum Option_qsort
{
	exit_qsort,
	name,
	age,
	sex,
	addr,
	tele
};
void menu_qsort()
{
	printf("*****************************\n");
	printf("***** 1、name   2、age  *****\n");
	printf("***** 3、sex    4、addr *****\n");
	printf("***** 5、tele   0、exit_sort*\n");
	printf("*****************************\n");
}


//按照名字进行排序
int Conpare_ByName(const void*e1, const void* e2)
{
	return strcmp( ((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name );
}

//按照年龄进行排序
int Conpare_ByAge(const void*e1, const void* e2)
{
	return ((PeoInfo*)e1)->age - ((PeoInfo*)e2)->age;
}

//按照性别进行排序
int Conpare_BySex(const void*e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->sex, ((PeoInfo*)e2)->sex);
}

//按照地址进行排序
int Conpare_ByAddr(const void*e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->addr, ((PeoInfo*)e2)->addr);
}

//按照电话进行排序
int Conpare_ByTele(const void*e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->tele, ((PeoInfo*)e2)->tele);
}

void print(Contact* pc)
{
	int i = 0;
	printf("%-10s\t%-5s\t%-5s\t%-12s\t%-50s\n", "名字", "年龄", "性别", "电话", "地址");

	for (i = 0; i < pc->sz; i++)
	{
		printf("%-10s\t%-5d\t%-5s\t%-12s\t%-50s\n", pc->data[i].name,
			pc->data[i].age,
			pc->data[i].sex,
			pc->data[i].tele,
			pc->data[i].addr);
	}
}
//排序联系人的信息
void SortContact(Contact* pc)
{
	int input = 0;
	do{
		menu_qsort();
		printf("请选择排序的方式:>");
		scanf("%d", &input);
		switch (input)
		{
			//按照名字进行排序
		case name:
			//这里求数组元素个数的时候,不要使用整个数组的大小比上数组首元素的大小,因为我们在这里排序的时候,仅对已添加的成员进行排序,比如添加的成员个数是3的话,
			//就对这3个元素进行排序,如果开辟的数组有1000个元素,但是只添加3个的话,如果使用整个数组的大小比上数组首元素的大小,求出来的就是1000,意思是对1000个元素进行排序
			//这样是不对的,应该对添加进去的3个元素进行排序,pc->sz,这个整体就已经代表了元素个数是3,不需要再计算了,所以,使用下面的代码即可。
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByName);
			print(pc);
			break;
			//按照年龄进行排序
		case age:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByAge);
			print(pc);
			break;
			//按照性别进行排序
		case sex:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_BySex);
			print(pc);
			break;
			//按照地址进行排序
		case addr:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByAddr);
			print(pc);
			break;
			//按照电话进行排序
		case tele:
			qsort(pc->data, pc->sz, sizeof(pc->data[0]), Conpare_ByTele);
			print(pc);
			break;
			//退出排序
		case exit_qsort:
			printf("退出排序\n");
			printf("\n");
			break;
			//选择错误
		default:
			printf("选择排列方式错误,请重新进行选择:>\n");
			break;
		}
	}while (input);
}


//清空所有联系人的信息
void ClearContact(Contact* pc)
{
	(pc->sz) = 0;
	memset(pc->data, 0, sizeof(pc->data));
	printf("清空成功\n");
	printf("\n");
	//在此过程把sz置为了0,再次打印的时候,不进入遍历的循环,不打印数据,直接出程序。
}



//销毁通讯录
void DestoryContact(Contact* pc)
{
	//释放
	free(pc->data);
	pc->data = NULL;
	pc->sz = 0;
	pc->capacity = 0;
}

//保存通讯录的信息到文件
void SaveContatct(const Contact* pc)
{
	//写文件
	FILE* pf = fopen("contact.dat", "w");
	if (pf == NULL)
	{
		//打开文件失败
		perror("SaveContatct");
		return ;
	}
	else
	{
		//打开文件成功,写入文件,以二进制的形式写入文件、
		int i = 0;
		for (i = 0; i < pc->sz; i++)
		{
			fwrite(pc->data + i, sizeof(PeoInfo), 1, pf);
		}
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
}

4.3 contact.h头文件

#include<stdio.h>
#include<string.h>
#include<stdlib.h>



//类型的定义、通讯录中要放1000个人的信息,每个人的信息要包括名字、年龄、性别、电话、地址等,所以要定义成一个结构体,而该结构体要在两个源文件中使用,所以最好定义在头文件中,这样只需要包含一下头文件就可以频繁的使用了。

//定义结构体类型:
#define MAX_NAME 20 //全大写
#define MAX_SEX 10
#define MAX_TELE 12
#define MAX_ADDR 30
#define MAX 1000



#define DEFAULT_SZ 3  //初始化联系人信息的个数为3、
#define INC_SZ 2      //增量为2、




typedef struct PeoInfo 
{
	//char name[20];
	//直接写成固定值就写死了,不方便后期的修改,所以使用#define 来定义一个常量,后期直接改变#define中的内容即可。
	char name[MAX_NAME];
	//同上,性别也使用#define来定义
	char sex[MAX_SEX];
	int age;
	char tele[MAX_TELE];
	char addr[MAX_ADDR];
}PeoInfo;//使用typedef对结构体类型重命名为:PeoInfo






//结构体嵌套
//通讯录—  动态、
typedef struct Contact //重命名
{
	//PeoInfo* data = (PeoInfo*)malloc(3 * sizeof(PeoInfo);//存放添加进去的联系人的信息
	//但是还是在一个结构体成员变量中,一般不初始化,直接写出等号左边的内容即可。
	PeoInfo* data;//结构体指针变量data指向在堆区上动态开辟的内存空间的起始位置的地址,存放联系人信息、
	int sz;//记录当前通讯录中有效信息的个数
	int capacity;//用来表示通讯录的最大容量、
}Contact;	
//将在堆区上动态开辟的内存空间的首元素的地址放在指针变量data中,并且使用整型变量sz去记录已有的有效的联系人的个数,其次再使用整型变量capacity来表示当前通讯录的最大容量;
//因为最初的时候,初始化该动态开辟的内存空间中能存放3个人的信息,所以说,capacity的初始值为当前最大的容量、






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

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

//显示联系人的信息
void PrintContact(const Contact* pc);
//const放在*的左边,指的就是指针变量pc指向的内容不可以被修改,pc指向了通讯录con,所以通讯录中的内容,即数组和sz都不可以发生改变,又因为联系人
//的每个信息都是数组中每一个元素的子集,现要求整个数组都不能改变,所以每个元素中的内容就更不能改变。

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

//查找联系人的信息
void SearchContact(const Contact* pc);

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

//排序联系人的信息
void SortContact(Contact* pc);
//如果在*左边加const,那么pc指向的内容就不能改变,pc指向了一个数组和sz,其中,排序的话,sz的个数没有变化, 但是,数组各元素之间会有排序,这个顺序就会导致
//数组的内容发生了改变,所以,不可以加const。


//清空所有联系人的信息
void ClearContact(Contact* pc);

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

//保存通讯录的信息到文件
void SaveContatct(const Contact* pc);


//加载文件内容到通讯录
void LoadContact(Contact* pc);

//检测是否需要增容
void Check_Capacity(Contact* pc);


通讯录动态内存分配和保存信息的版本的基本功能已经实现,如果对您有帮助,记得点赞收藏,谢谢各位~

猜你喜欢

转载自blog.csdn.net/lcc11223/article/details/122732153