用顺序表写一个小项目——通讯录(原文近9000字,图文并茂,干货满满详细解读)

本文介绍了如何使用C语言中的顺序表数据结构实现一个简单的通讯录项目,包括添加、删除、查找、修改和查看联系人功能,以及编程学习过程中的一些体会和建议。
摘要由CSDN通过智能技术生成


在这里插入图片描述


一、前言

PS:本人的代码块和代码文档中的注释有包含自己初学时的自己的理解和笔记,若有不对之处,还请读者理解包容,欢迎指出( ̄︶ ̄)

**在学校时,我们最经常接触的信息管理系统是学生管理系统。比如刚开学的时候,作为新生,我们要录入很多信息到学校的信息系统中以方便管理。而几千个人的信息如何管理,用什么管理便是个问题。联想我们学过的数据结构:顺序表便是优选之一,每个人的信息都是一个信息块,一个一个人的信息放置下来,需要的时候点开系统,姓名,年龄,性别,学号,生源地便一目了然了。而我们常用的手机通讯录也是这样的,谁谁谁的名字,性别,电话号码,住址也是用顺序表存储。由此,怎样把我们学过的顺序表做成一个通讯录呢?
**

二、具体功能

PS:(前期的准备)的时候先展示代码再配合文字解说细节。(实现功能的函数)代码块放在文字解说上方,两者结合,“食用效果”更佳哦。

0.前期的准备

a.两份写过的顺序表代码(有不理解的可以阅读我之前发过的“CSDN小白一文详细解读顺序表”哦)
SqeList.c

#include"SeqList.h"
//初始化函数
void SLInit(SL* ps) {
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

//扩容函数
void SLCheckCapacity(SL* ps) {
	if (ps->size == ps->capacity) {
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
		if (tmp == NULL) {
			perror("realloc fail!");
			exit(1);//相当于return 1
		}//成功扩容
		ps->arr = tmp;//动态内存开辟的内容,指针指向的空间赋给数组
		ps->capacity = newCapacity;
	}
}

//销毁函数
void SLDestroy(SL* ps) {
	assert(ps);
	if (ps->arr) {
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

//打印函数
void SLPrint(SL* ps) {
	for (int i = 0; i < ps->size; i++) {
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}
//1.头插函数
void SLPushFront(SL* ps, SLDataType x) {
	assert(ps);
	//判断是否扩容
	SLCheckCapacity(ps);

	//旧数据往后移动一位
	for (int i = ps->size; i >= 1; i--) {
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	ps->size++;
}
 	   
//2.尾插函数
void SLPushBack(SL* ps, SLDataType x) {
	assert(ps);
	SLCheckCapacity(ps);
	ps->arr[ps->size++] = x;
}
//3.头删函数
void SLPopFront(SL* ps) {
	assert(ps);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

//4.尾删函数
void SLPopBack(SL* ps) {
	assert(ps);
	assert(ps->size);
	ps->arr[ps->size - 1] = -1;
	ps->size--;
}
 //5.指定位置插入(三组测试,三种结果)
void SLInsert(SL* ps, int pos, SLDataType x) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);

	for (int i = pos; i <= ps->size + 1; i++) {
		ps->arr[i + 1] = ps->arr[i];
	}
	ps->size++;
	ps->arr[pos] = x;
}
//6.删除指定数据
void SLErase(SL* ps, int pos) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	for (int i = pos; i <= ps->size - 2; i++) {
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}
//8.修改指定位置的数据
void SLChange(SL* ps, int pos, SLDataType x) {
	ps->arr[pos] = x;
}

SeqList.h

#define _CRT_SECURE_NO_WARNINGS
#pragma once//防止重复编译
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "Contact.h"//包好所有头文件
typedef Info SLDataType;//重命名int类型,名为顺序表数据类型
typedef struct SeqList {
	SLDataType* arr;//存储顺序表的底层结构,数组,定义指向数组的指针
	int capacity;//定义结构体大小,顺序表的空间大小
	int size;//定义数组大小
}SL;
//初始化函数(注意,头文件中包含的是所有调用函数的声明,相当于转化成
//库函数可以在主函数中直接调用)
void SLInit(SL* ps);
//销毁函数
void SLDestroy(SL* ps);
//打印函数
void SLPrint(SL* ps);
//1.头插函数
void SLPushFront(SL* ps, SLDataType x);
// 2.尾插
void SLPushBack(SL* ps, SLDataType x);
// 3.头删
void SLPopFront(SL* ps);
// 4.尾删
void SLPopBack(SL* ps);
// 5.指定位置插入(三组测试,三种结果)
void SLInsert(SL* ps,int pos, SLDataType x);
//6.删除指定数据
void SLErase(SL* ps,int pos);
8.修改指定位置的数据
void SLChange(SL* ps, SLDataType pos, SLDataType x);

b.通讯录的头文件声明contact.h。其中不仅包括了有我们要实现的各种功能,还有我们的通讯录结构体类型。可以这样理解,在顺序表那篇文章中,我们信息块是一个又一个数字,而这里我们把数字换成了每个联系人的信息块,也就是自定义的嵌套在一大本通讯录的每一个联系人的结构体。简而言之,存的不只是一个一个离散的数字了。
Contact.h

#define _CRT_SECURE_NO_WARNINGS
#pragma once

#define NAME_MAX 100
#define GENDER_MAX 10
#define TEL_MAX 12
#define ADDR_MAX 100


//通讯录数据类型
typedef struct PersonInfo 
{
	char name[NAME_MAX];
	int age;
	char gender[GENDER_MAX];
	char tel[TEL_MAX];
	char addr[ADDR_MAX];
}Info;//这里注意,把typedef后和Info前的内容当成一个主体重命名,即
//struct PersonInfo,注意这里的Info不是结构体变量!!是一个重命名的名字
// //顺序表的前置声明(在别的源文件,相当于函数的使用声明)
struct SeqList;
typedef struct SeqList Contact;
//函数的声明
void ContactInit(Contact* pcon);//实际初始化的还是顺序表
void ContactDesTroy(Contact* pcon);
void ContactAdd(Contact* pcon);
void ContactDel(Contact* pcon);
void ContactModify(Contact* pcon);
void ContactFind(Contact* pcon);
void ContactShow(Contact* pcon);


所以,大家要注意一个小细节,在SqeList.h要把typedef int改成info,以达到我们存储联系人信息的目的
在这里插入图片描述

此外,大家看到我这里用了宏变量来定义联系人结构体中数组成员的大小,是因为我们为了以后一次性更改方便。举个例子,要是我们以后在工程中写了100处的name【】里面的值,那我改100次不是费时费力吗?所以,我们直接在define全改,非常轻松便利。
在这里插入图片描述

c.我们所设置的contest文件有测试功能,包含着主函数。而contact.c里面就是我们完整的要实现的函数的内容了,且听我慢慢道来
d.还要注意一点,这里要注意我们要怎么包含文件,如果没有正确包含,那编译器会因为头文件互相包含而报错。在这里最好是SqeLIst.h包含contact.h。这样可以避免info结构体前置声明问题,如果反着包含,编译器会不认识info。

截图

在这里插入图片描述

1.添加测试界面

首先,我们平常用手机通讯录的时候,都会有一个菜单,我们可以仿着做一个,这样的参考引导界面。
其次,写一个do while循环来让使用者选择他们的操作,并用分支语句,进入不同的函数。
最后,别忘了销毁通讯录,否则会有内存泄露的问题,毕竟我们malloc数组过了。

#include "SeqList.h"

//菜单

void menu() {
	printf("*****************通讯录***************\n");
	printf("*******1.添加联系人  2.删除联系人*****\n");
	printf("*******3.修改联系人  4.查找联系人*****\n");
	printf("*******5.查看通讯录  0.退出      *****\n");
}
int main()
{
	int op = -1;
	Contact con;
	ContactInit(&con);//通讯录初始化
	//操作
	do {
		menu();
		printf("请输入您的操作:\n");
		scanf("%d", &op);
		switch (op)
		{
		case 1:
			//添加联系人
			ContactAdd(&con);
			break;
		case 2:
			//删除联系人
			ContactDel(&con);
			break;
		case 3:
			//修改联系人
			ContactModify(&con);
			break;
		case 4:
			//查找联系人
			ContactFind(&con);
			break;
		case 5:
			//查看通讯录
			ContactShow(&con);
			break;
		case 0:
			//退出通讯录
			printf("通讯录退出中...\n");
			break;
		default:
			printf("选择错误,重新输入");
		}
	} while (op);

		//销毁通讯录
		ContactDesTroy(&con);
	return 0;
}

2.添加联系人

基于顺序表的操作,我们分析,大体的思路是在通讯录函数中调用顺序表中的函数去实现增删查改的功能。而函数的参数正是我们通俗意义上的接口,像是两根数据线接在一起以实现信息传递。不过这里是功能上的交互和迁用而已。
故而,我们定义一个联系人结构体,逐个输入信息。最后尾插进顺序表中,即完成。

void ContactAdd(Contact* pcon) {
	Info info;//对比头文件中的解说我们知道,
	//这里相当于struct PersonInfo,所以这句话的意思是
	//定义结构体info
	printf("请输入联系人的姓名:\n");
	scanf("%s", info.name);
	printf("请输入联系人的年龄:\n");
	scanf("%d", &info.age);
	printf("请输入联系人的性别:\n");
	scanf("%s", info.gender);
	printf("请输入联系人的电话:\n");
	scanf("%s", info.tel);
	printf("请输入联系人的住址:\n");
	scanf("%s", info.addr);
	//保存插入的联系人信息,将SLDateType类型数据改成
	//Info结构体类型,不是int类型,在SeqList.h中
	SLPushBack(pcon, info);
}

3.删除联系人

有了上面的经验,我们继续研究如何删除。既然要删除,第一肯定是要有。所以,我们得先分装一个查找函数来告诉使用者你找的这个人在不在。所以,建立一个返回值为下标或-1的查找函数来达成我们查找的目的。
事实上,查找函数我们在后面修改联系人的时候还要再用一次,这也是为什么我们选择分装函数,从而不用CV,达成“偷懒”的目的。
删除某个联系人,刚好顺序表就有删除函数,直接拿现成的就能实现。

int FindByName(Contact* pcon, char name[]) {
	for (int i = 0; i < pcon->size; i++)
	{
		if (strcmp(pcon->arr[i].name, name)==0)
		return i;
	}
	return -1;
}

//删除联系人
void ContactDel(Contact* pcon){
	printf("请输入要删除的联系人\n");
		//注意,删除之前,要先确定联系人存在(先查找)
		char name[NAME_MAX];
	scanf("%s", name);
	int findIndex = FindByName(pcon, name);
	if (findIndex < 0) //i,-1判断体系有助于后续操作
	{
		printf("要删除的联系人不存在!\n");
		return;
	}
	SLErase(pcon, findIndex);
	printf("联系人删除成功!\n");
}

4.查找联系人

趁热打铁,恰巧我们现在要找人,那直接把的分装查找函数一调用。找不到就报告找不到,找到了就打印信息。一个巧妙的设计不知道大家有没有发现,返回下标让我们能直接拿着这把“钥匙”,找到我们要查的人。也就是说,他对应数组的元素也能知道,直接走一波结构体访问成员,就能打印出所有信息了。

//查找联系人
void ContactFind(Contact* pcon)
{
	char name[NAME_MAX];
	printf("请输入你要查找的用户名:\n");
	scanf("%s", name);
	int findIndex = FindByName(pcon, name);
	if (findIndex < 0) //i,-1判断体系有助于后续操作
	{
		printf("要查找的联系人不存在!\n");
		return;
	}
	printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "住址");
	printf("%s %s %d %s %s\n", 
		pcon->arr[findIndex].name,
		pcon->arr[findIndex].gender,
		pcon->arr[findIndex].age,
		pcon->arr[findIndex].tel,
		pcon->arr[findIndex].addr);
	
}

5.修改联系人

修改一样,先找人再说,找到了就能改,简单一个scanf,直接把原来信息用新信息一个一个覆盖就好办,最后温馨提示一下修改成功,整个通讯录就完成八九成了

void ContactModify(Contact* pcon) {
	char name[NAME_MAX];
	printf("请输入你要修改的联系人姓名:\n");
	scanf("%s", name);
	int findIndex = FindByName(pcon, name);
	if (findIndex < 0) //i,-1判断体系有助于后续操作
	{
		printf("要修改的联系人不存在!\n");
		return;
	}
	printf("请输入联系人的姓名:\n");
	scanf("%s", pcon->arr[findIndex].name);
	printf("请输入联系人的年龄:\n");
	scanf("%d", &(pcon->arr[findIndex].age));
	printf("请输入联系人的性别:\n");
	scanf("%s", pcon->arr[findIndex].gender);
	printf("请输入联系人的电话:\n");
	scanf("%s", pcon->arr[findIndex].tel);
	printf("请输入联系人的住址:\n");
	scanf("%s", pcon->arr[findIndex].addr);
	//和上面的增加联系人一样,但是是修改
	printf("联系人修改成功\n");
}

6.查看通讯录

一次性全显示,只要一波循环,换行+访问,比较轻松。

void ContactShow(Contact* pcon) 
{
	printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "住址");
	for (int i = 0; i < pcon->size; i++)
	{
		printf("%s %s %d %s %s\n",
		pcon->arr[i].name,
		pcon->arr[i].gender,
		pcon->arr[i].age,
		pcon->arr[i].tel,
		pcon->arr[i].addr);
	}

}

测试结果展示
在这里插入图片描述

在这里插入图片描述

三、学习总结

通讯录和学生信息系统是很有意义的实践应用。至此,顺序表的学习就暂时告一段落了。但是,我们并不会把它晾在一边,在栈和队列,以己二叉树的堆中,我们还会用到这样的顺序结构。而我们下一章研究的链表,又是另一种很有意思的结构。它也有创建和销毁,增删查改,虽然两者具有的功能相像,但事实上还是相当不相同的。毕竟物理不连续但逻辑连续。上面的介绍是一种非文件类型的通讯录,其缺点在于不能够保存你写进的数据。下次点开的时候,就没有任何信息了。所以呢,可以参考其他博主写的文件版的顺序表。另一方面,它还有单链表的版本,同样也能实现,鼓励大家多多尝试,有所学,必有所收获,必有所得。

四、浅聊编程学习之路

学好数据结构,死磕代码,磕成这样就可以了:
在这里插入图片描述

编程学习并非一步而就,除非天才,否则很难实现在很短的时间练就代码能力,成为技术大牛,他需要的是时间的复利和积累,由量变引发质变。在不断往后的学习中回顾前面学习的知识,并加以深化和思考化为内功。写博客一方面,内化编程技术的零碎知识,对学过的内容系统整理有益于自己。另一方面,分享技术,交流优点和改进不足,同时促进博主和读者共同进步,很有意义。除此之外,写博客也是对自己的写作能力,表达能力和逻辑能力的一种历练。这是Csdn 小白写的第三篇博客,如果喜欢,欢迎三连点赞,互关必回哦。让我们一起在技术上技术和成长,锻炼,成就更好的自己吧。我们下期再见!┏(^0^)┛

  • 65
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值