C语言——通讯录管理系统2.0版

news2024/11/29 2:41:11

对比之前C语言——通讯录管理系统初始版本,2.0版本有以下优化:
1.采用链表实现(之前版本是顺序表实现的,导致通讯录容量有限,现在使用链表实,实现了动态开辟空间,不浪费空间,也不会出现空间不够用的情况)
2.实现了文件的读取和保存(程序开始前会读取磁盘中已有的通讯录文件信息,程序结束后会保存内存中的通讯录信息到磁盘文件)
3.程序界面也有一定优化

项目效果展示

在这里插入图片描述
实现文件存储
在这里插入图片描述

一、 项目要求整理

  1. 多文件方式实现
  2. 通讯录中每个成员的信息都包含姓名电话地址等内容(可以根据需要自行决定通讯录成员内容信息)
  3. 实现添加、删除、修改、查找、展示联系人信息的功能
  4. 将通讯录保存在文件中,若不是第一次运行程序,那么每次运行程序时会自动读取之前保存的信息
  5. 能对用户的错误输入进行反馈

二、代码文件整理

在这里插入图片描述
通讯录2.0版本这里用了5个文件来实现

  • people.h:内定义了联系人应该包含的基本信息的结构体,还有一些和联系人结构体函数有关的声明
#define _CRT_SECURE_NO_WARNINGS 1

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

typedef struct People
{
	char name[20];
	char phone[14];
	char address[20];
}PEO, * PPEO;

//宏定义打印格式
#define FORMAT "%-10s %-20s %-10s\n"

void printPeopleInfo(PEO people);//打印联系人信息

void inputPeopleInfo(PPEO people);//录入联系人信息

int cmpByName(PEO data1, PEO data2);//比较两个联系人的姓名是否一致,是一致返回1,不是一致返回0
  • SqList.h:单链表的结构体定义和相关函数的声明
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include"people.h"

typedef struct People elemType;

typedef struct SqList
{
	elemType data;
	struct SqList* next;
}SqList, * pList;



pList CreatList();//创建一个带头结点的单向链表
pList BuyNode(elemType data);//创建一个节点
void HeadAdd(pList list, elemType data);//向链表中插入数据,头插法
void Delete(pList list, elemType x);//删除链表中的数据x
void Change(pList list, elemType x, elemType newdata);//修改数据
size_t Find(pList list, elemType x);//查找数据X,返回节点位序
void Destroy(pList list);//销毁链表
void Print(pList list);//打印链表中的数据
  • people.c:对应people.h文件中函数的实现
    在这里插入图片描述

  • SqList.c:对应SqList.h文件中函数的实现
    在这里插入图片描述

  • test.c:测试文件,内含通讯录主体函数,还有main()函数

在这里插入图片描述

三、代码逻辑梳理

3.1 主函数main()函数

我们先从main()函数开始,像剥洋葱似的,一层一层往下完善。
Alt
main()函数内容十分简答,里面就一个test()测试函数,

int main()
{
	test();
	return 0;
}

因为这个程序是打算把通讯录程序的主要运行代码放在test()函数中去,这是一个良好的代码习惯,把测试代码单独封装成一个函数,有很多好处:

  1. 代码组织和可读性:将测试代码从主要的业务逻辑中分离出来可以使代码更加清晰和易于理解。这也有助于在团队环境中工作,因为不同的开发人员可以同时处理不同的代码部分。
  2. 可重用性:如果测试代码是单独封装的,那么它可以轻松地在其他地方重复使用,而不需要复制和粘贴。
  3. 模块化:将代码分解为较小的、独立的函数或模块可以使代码更容易维护和更新。这也符合“单一职责原则”,即每个函数或类应该只做一件事情。
  4. 测试的独立性:如果测试代码和主要代码混在一起,那么测试可能会依赖于某些特定的实现细节,这可能会导致测试的可靠性和稳定性降低。将测试代码封装在一个独立的函数或模块中可以确保测试的独立性。
  5. 易于调试和错误排查:如果所有的测试代码都在一个单独的函数或模块中,那么你可以很容易地找到并修复任何问题。
  6. 遵循最佳实践:许多现代编程语言和开发最佳实践都推荐将测试代码与主要代码分开。

3.2 完善test()函数

然后我们继续完善test()函数,test()函数主要内容是一些测试通讯录的函数,

void test()
{
	//创建一个通讯录链表
	pList contact = CreatList();
	//读取磁盘文件到内存(如果有文件的话)有文件读取文件内容,没有文件创建文件,所以采用‘w’方式打开文件
	readFile(contact, "contact.txt");
	do
	{
		//打印通讯录菜单界面
		menu();
		//读取用户输入,根据用户输入跳转到相应函数;之中包含了退出通讯录时保存数据
		keydown(contact);
		system("pause");
		system("cls");
	} while (1);
}

test()函数是维护通讯的函数,一整个程序我们都是在维护一个单链表。所以,test()函数首先通过单链表中的函数CreatList() 创建一个带头结点的单链表并完成该单链表的初始化,这里取名为contact ,他的类型是结构体pList 类型(结构体指针类型)。

接着,就是首先要从磁盘文件中读取数据,如果之前有存储联系人信息的话,就把联系人信息先读取到内存中,然后插入到创建好的单链表中,完成文件的读取。这里把从磁盘中读取文件内容到链表中的操作封装成一个函数 readFile ;函数的返回值为void,函数的参数有两个,分别是单链表的头指针list和要读取的文件名filename

这里还考虑了文件不存在的情况(即第一次使用通讯录时。磁盘中没有通讯录文本文件),文件不存在时,就在当前代码路径下创建一个通讯录文本文件。

void readFile(pList list, const char* filename)
{
	FILE* pf = fopen(filename, "r");//以只读的形式打开文件,如果文件不存在,则打开文件失败,返回NULL指针
	if (pf == NULL)//如果返回NULL说明不存在此文件,这里用“w”方式打开,就会创建一个文件
	{
		pf = fopen(filename, "w");
		fclose(pf);
		return;
	}
	else
	{
		PEO temp = { 0 };//初始化结构体
		while (fscanf(pf, "%s%s%s", temp.name, temp.phone, temp.address) != EOF)//这里写%-10s%-10s%-10s会报错,写宏定义FORMAT也会报错
		{
			HeadAdd(list, temp);
		}
		fclose(pf);
	}
}

读取磁盘文件结束后就是正式的进入通讯录管理系统的主要界面了↓↓↓↓↓↓
在这里插入图片描述
主要运行程序通过一个do-while循环实现,显示打印选择菜单,然后是用switch分支语句,根据用户的输入来跳转到指定操作的函数中去,为了保证界面的整洁和美观,一个操作结束后,我们先用system("pause")提示用户输入任意键继续,接着用system("cls")命令进行清屏处理。
在这里插入图片描述
然后就又是一样,打印菜单,获取用户输入,跳转到指定操作,完成操作后输入任意键,清屏-----直到用户输入0,或者用户输入6 的时候,跳转到指定操作后,内部有exit(0)退出程序命令,会使得程序结束运行。

代码如下:

void keydown(pList list)
{
	int choose = -2;
	printf("请输入你的选择(0--6):");//这里用户如果输入字符程序会陷入死循环,只能输入数字,这是一个小bug
	scanf("%d", &choose);
	switch (choose)
	{
	case 0:
		printf("正常退出\n");
		//保存内存中的信息到文件中
		saveToFile(list, "contact.txt");
		exit(0);
		break;
	case 1:
		AddPeople(list);
		break;
	case 2:
		ShowContact(list);
		break;
	case 3:
		DeltePeople(list);
		break;
	case 4:
		FindPeople(list);
		break;
	case 5:
		ChangePeople(list);
		break;
	case 6:
		DestroyContact(list);
		exit(0);
		break;
	default:
		printf("输入错误,请重新输入\n");
		break;
	}
}

3.3 补全keydown()函数

代码写到这里,程序的基本架构已经有了,接下来就是补全程序的功能了,也就是把对应的函数实现了。
在这里插入图片描述

void saveToFile(pList list, const char* filename) //保存内存中数据到磁盘文件中

首先是saveToFile 函数,这里的情景是,如果用户输入0的话,意思就是用户想要退出程序,这时就要把内存中的数据保存到磁盘文件中去。

这个函数有两个参数,分别是单链表的头指针和文件名称,返回值是void,内容是首先是fopen打开文件,然后是遍历链表,用fprintf函数把链表中每个节点在的数据保存到文件中,最后关闭文件指针,输出提示“保存文件成功”。

代码如下:

void saveToFile(pList list, const char* filename)
{
	FILE* fp = fopen(filename, "w");
	SqList* pmove = list->next;
	if (fp == NULL)
	{
		printf("文件保存失败\n");
		return;
	}
	while (pmove != NULL)
	{
		fprintf(fp, FORMAT, pmove->data.name, pmove->data.phone, pmove->data.address);
		pmove = pmove->next;
	}
	fclose(fp);
	printf("文件保存成功\n");
}

AddPeople(pList list) //添加联系人

程序一开始就是基于单链表实现的,所以添加联系人就可以利用单链表的头插法函数接口,直接使用头插法,现在需要获得链表节点的数据域,就是需要获取用户输入,之前在people.hpeople.c文件中有准备相关函数

AddPeople(pList list)
{
	PEO data = { 0 };
	inputPeopleInfo(&data);
	HeadAdd(list, data);
}

首先我们先创建一个PEO类型的结构体变量data,用来存储用户输入的联系人信息,我们先初始化为0;接着调用inputPeopleInfo 函数,获取用户输入的联系人信息到变量data中。

void inputPeopleInfo(PPEO people)
{
	printf("请输入联系人的姓名:");
	scanf("%s", people->name);

	printf("请输入联系人的电话:");
	scanf("%s", people->phone);

	printf("请输入联系人的地址:");
	scanf("%s", people->address);

}

之后就是熟悉的带头结点的链表的头插法了,HeadAdd 带头结点的单链表的头插法,先创建新节点,把新节点的next指针指向原来的单链表的第一个有效节点,之后再处理头节点的next指针的指向,让其指向新插入的节点。

//在SqList.h文件中有编辑,typedef struct People elemType;这里的elemtype就是PEO
void HeadAdd(pList list, elemType data)
{
	assert(list);
	pList node = BuyNode(data);
	node->next = list->next;
	list->next = node;
	printf("Add success \n");
}

void ShowContact(pList list) //显示所有联系人/打印通讯录

直接上代码

void ShowContact(pList list)
{
	if (list == NULL)
	{
		printf("通讯录为空,没有数据\n");
		return;
	}
	printf(FORMAT, "姓名", "电话", "地址");
	Print(list);//调用打印链表的函数接口
}

打印链表,循环遍历链表的节点,打印数据域中的内容

void Print(pList list)
{
	pList pcur = list->next;
	while (pcur)
	{
		printPeopleInfo(pcur->data);//调用打印PEO结构体数据的函数,在people.c文件中实现的
		pcur = pcur->next;
	}
}
void printPeopleInfo(PEO peole)
{
	printf(FORMAT, peole.name, peole.phone, peole.address);
}

void DeltePeople(pList list) //删除联系人

其实关于通讯录中的添加联系人、删除联系人、修改联系人、查找联系人、清空联系人都对应单链表的插入数据、删除数据、修改数据、查找数据、销毁链表的操作,所以完全可以利用链表的函数接口,对接口进行小小的修改就好。

这里就不再赘述,直接上代码。

void DeltePeople(pList list)
{
	PEO people = { 0 };
	printf("请输入你要删除的联系人的姓名:");
	scanf("%s", people.name);
	Delete(list, people);
}

使用了链表中删除元素的函数:

void Delete(pList list, elemType x)
{
	assert(list);
	pList pcur = list->next;
	pList prev = list;
	while (pcur)
	{
		if (cmpByName(pcur->data,x))//比较用户输入的姓名和链表节点中的姓名是否一致,是一致返回1,不是一致返回0
		{
			prev->next = pcur->next;
			free(pcur);
			pcur = NULL;
			printf("Delete success\n");
			return;
		}
		prev = prev->next;
		pcur = pcur->next;
	}
	printf("Delete fail\n");
}
int cmpByName(PEO data1, PEO data2)
{
	if (strcmp(data1.name, data2.name) == 0)
	{
		return 1;
	}
	else
		return 0;
}

void FindPeople(pList list) //查找联系人

和删除联系人的操作类似,也是用链表的查找节点函数

void FindPeople(pList list)
{
	PEO people = { 0 };
	printf("请输入你要查找的联系人的姓名:");
	scanf("%s", people.name);
	Find(list, people);
}

当时实现单链表的时候,这个函数本来是要返回节点在链表中的位序的,现在可以不用返回位序,稍稍修改↓↓↓↓↓

size_t Find(pList list, elemType x)
{
	assert(list);
	int count = 1;
	pList pcur = list->next;
	while (pcur)
	{
		if (cmpByName(pcur->data, x))
		{
			printf(FORMAT, "姓名", "电话", "地址");
			printf(FORMAT, pcur->data.name, pcur->data.phone, pcur->data.address);
			printf("Find success\n");
			return count;
		}
		pcur = pcur->next;
		count++;
	}
	printf("Find fail\n");
}

void ChangePeople(pList list) //修改联系人信息

关于修改联系人信息,这里涉及到要用户重新输入联系人信息的操作,所以这里要再调用录入联系人信息的函数inputPeopleInfo

void ChangePeople(pList list)
{
	PEO people = { 0 };
	printf("请输入你要修改的联系人的姓名:");
	scanf("%s", people.name);
	PEO newdata = { 0 };
	inputPeopleInfo(&newdata);
	Change(list, people, newdata);
}
void Change(pList list, elemType x, elemType newdata)
{
	assert(list);
	pList pcur = list->next;
	while (pcur)
	{
		if (cmpByName(pcur->data, x))
		{
			pcur->data = newdata;
			printf("Change success\n");
			return;
		}
		pcur = pcur->next;
	}
	printf("Change fail\n");
}

void DestroyContact(pList list) //清空联系人/销毁通讯录

这里用Destroy()销毁单链表后,但是磁盘文件中还是有之前联系人的信息,为了把磁盘中的文件中的存储的信息也销毁,这里用fopen"w"(只写)的方式打开文件,这样会把原有的文件内容给覆盖掉,然后就fclose()关闭文件,DestroyContact结束后就exit(0)退出程序。

至于为什么要退出程序,是因为我在测试程序的时候发现,销毁通讯录后再进行其他操作都会使得程序崩溃,这是一个BUG,我目前没有办法解决;同时我也发现,如果用exit(0);退出程序后,再进入程序就还能正常运行,所以,我这里设定,销毁通讯录后退出程序。

void DestroyContact(pList list)
{
	Destroy(list);
	printf("通讯录销毁,联系人清空\n");
	//新建磁盘文件,覆盖掉原来的文件
	FILE* fp = fopen("contact.txt", "w");
	if (fp == NULL)
	{
		printf("文件保存失败\n");
		return;
	}
	fclose(fp);
	list = NULL;
}
void Destroy(pList list)
{
	pList pcur = list->next;
	pList next = NULL;
	while (pcur)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(list);
	list = NULL;
}

四、总结

至此我们的”洋葱“总算是一层一层剥完了。
请添加图片描述

这个项目让我初步体会到,一些函数接口的妙用。比如说这个小项目,分成了5个文件,单链表的2个文件、描述联系人信息数据的2个文件,这些文件事先已经实现了很多函数。

之后在test.c文件中就只用调用已有的函数接口就可以了,这让代码逻辑变得很清楚,我差不多能想象一个项目多人合作的场景了,大家写的不一样的文件,但是最后都可以互相调用。实在是妙啊~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1174750.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用于3D Visual Grounding的多模态场景图

文章目录 引言方法1. Language Scene Graph Module Paper&#xff1a;《Free-form Description Guided 3D Visual Graph Network for Object Grounding in Point Cloud》【ICCV’2021】 Code&#xff1a;https://github.com/PNXD/FFL-3DOG 引言 3DVG任务有以下三个挑战&#x…

关于笔记平台的使用感受分享

关于笔记平台的使用感受分享 前言我用过的笔记平台笔记平台简单评价巴拉巴拉WPS文档/OneNote/TowerNotion/语雀各种博客平台 个人使用率最高的平台 前言 最近也有部分同学问我平常用的笔记平台是什么&#xff0c;以及我比较推荐的平台是什么。这里不是广告哈&#xff0c;因为我…

Spring AOP 简介

一、Spring AOP AOP 是一种思想&#xff0c;而 Spring AOP 是一个框架&#xff0c;提供了一种对 AOP 思想的实现。 1、什么是 AOP&#xff1f; AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff1a;是一种编程思想&#xff0c;表示面向切面编程。指的是对某…

<Vue>使用依赖注入的方式共享数据

什么是vue依赖注入&#xff1f; Vue是一个用于构建用户界面的渐进式框架。 它提供了一种简单而灵活的方式来管理组件之间的数据流&#xff0c;即依赖注入&#xff08;Dependency Injection&#xff0c;DI&#xff09;。 依赖注入是一种设计模式&#xff0c;它允许一个组件从另一…

【数组】有序数组的平方

## 977.有序数组的平方 力扣题目链接 (opens new window) 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,10]输出&#xff1a;[0,…

升级Python版本后,anaconda navigator启动失败

anaconda navigator启动失败&#xff0c;尤其是重装不解决问题的&#xff0c;大概率是库冲突 1.通过anaconda-navigator的图标启动&#xff0c;没有反应 2.在命令窗口&#xff0c;输入anaconda-navigator&#xff0c;报错如下 anaconda-navigator 3.错误来自这里 File &quo…

基于单片机的胎压监测系统的设计

收藏和点赞&#xff0c;您的关注是我创作的动力 文章目录 概要 一、系统整体设计方案二、 系统设计4.1 主流程图 三 系统仿真5.1 系统仿真调试实物 四、 结论 概要 本文以STC89C52单片机为控制核心&#xff0c;通过气压传感器模块对汽车各轮胎的胎压进行实时数据的采集与处理&…

AD教程 (七)元件的放置

AD教程 &#xff08;七&#xff09;元件的放置 第一种放置方法 点击右下角Panels&#xff0c;选择SCH Library&#xff0c;调出原理图库器件列表选中想要放置的元件&#xff0c;点击放置&#xff0c;就会自动跳转到原理图&#xff0c;然后放置即可这种方法需要不断打开元件库…

【源码解析】Spring Bean定义常见错误

案例1 隐式扫描不到Bean的定义 RestController public class HelloWorldController {RequestMapping(path "/hiii",method RequestMethod.GET)public String hi() {return "hi hellowrd";}}SpringBootApplication RestController public class Applicati…

立创eda专业版学习笔记(7)(阻焊开窗)

阻焊开窗是什么&#xff1f; 在介绍阻焊开窗之前&#xff0c;我们首先要知道阻焊层是什么。阻焊层是指印刷电路板子上要上油墨的部分&#xff0c;用于覆盖走线和敷铜&#xff0c;以保护PCB上的金属元素和防止短路。阻焊开窗是指在阻焊层上开一个口&#xff0c;以便在开口的位置…

前馈神经网络自动梯度计算和预定义算子

目录 1 自动梯度计算和预定义算子 1.1 利用预定义算子重新实现前馈神经网络 1.2 完善Runner类 1.3 模型训练 1.4 性能评价 1.5 增加一个3个神经元的隐藏层&#xff0c;再次实现二分类&#xff0c;并与1.1.1做对比. 1.6 自定义隐藏层层数和每个隐藏层中的神经元个数&#xf…

【网络安全技术】公钥密码体制

一、两种基本模型 1.加密模型 A要给B发信息&#xff0c;那就拿B的公钥加密&#xff0c;传给B&#xff0c;B收到后会拿他自己的私钥解密得到明文。 2.认证模型&#xff08;数字签名&#xff09; A用自己的私钥加密&#xff0c;传输之后&#xff0c;别人拿A的公钥解密&#xff…

IMU漂移相关

个人对IMU的漂移一直以来都很困惑&#xff0c;总结整理了这些材料&#xff0c;希望能理清楚一点思路。 总的来讲&#xff0c;IMU的漂移可建模为三部分&#xff0c;随机常值相关漂移白噪声&#xff0c; 但实际使用时&#xff0c;三者都出现的用法很少。严恭敏老师在博客中有相关…

cp没有--exclude选项!Linux复制文件夹时如何排除一些文件?

之前使用tar命令压缩文件将时&#xff0c;使用了–exclude选项排除了一些不需要的文件。现在我想复制一个文件夹&#xff0c;但是其中一些文件不需要复制&#xff0c;此时注意到cp命令居然没有–exclude选项。 rsync可以快速地帮助我们完成相同的事情&#xff0c;命令如下&…

Android Framework学习之Activity启动原理

Android Activity启动原理 Android 13.0 Activity启动原理逻辑流程图如下&#xff1a;

排序——选择排序

基本思想 每一趟在待排序元素中选取关键字最小的元素加入有序子序列。 算法代码 #include <iostream> using namespace std;//选择排序 void SelectSort(int nums[],int n){int i,j,min;for(i0;i<n-1;i){ //一共需要进行 n-1 趟 mini; //记录最小元素的下…

RK3566上运行yolov5模型进行图像识别

一、简介 本文记录了依靠RK官网的文档&#xff0c;一步步搭建环境到最终在rk3566上把yolov5 模型跑起来。最终实现的效果如下&#xff1a; 在rk3566 板端运行如下app&#xff1a; ./rknn_yolov5_demo model/RK356X/yolov5s-640-640.rknn model/bus.jpg其中yolov5s-640-640.r…

【GEE】​3、 栅格遥感影像波段特征及渲染可视化

1、简介 在本单元中&#xff0c;将学习以下内容&#xff1a; 使用遥感传感器捕获的不同类型的能量。如何构建 JavaScript 字典和列表以选择单个栅格波段。如何可视化多波段和单波段栅格的不同组合。 2、背景 在您探索如何将 Google 地球引擎和遥感数据集成到您的研究中时&…

测试员如何快速熟悉新业务?

身处职场&#xff0c;学习新业务在所难免&#xff0c;尤其是测试人员&#xff0c;具备良好的业务知识是我们做好质量保障的前提&#xff0c;不管是职场「新人」还是「老人」&#xff0c;快速熟悉业务的能力都是不可或缺的&#xff0c;这是我们安身立命的根本。 但&#xff0c;…

简答-【1 绪论】

关键字&#xff1a; 数据类型、数据结构定义、递归关键、线性结构、非线性结构、算法特性、算法目标、时间复杂度排序