IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C语言:动态内存分配,来一个动态的通讯录 -> 正文阅读

[C++知识库]C语言:动态内存分配,来一个动态的通讯录

前言:本人为C语言初学者,学识尚浅,研究程度存在很大的局限性,眼界很窄。以下所有观点仅代表个人见解和思路,各位游刃有余的前辈可以给予批评和指正!各位与鄙人同路的学子可相互探讨、发表看法,交换观点!

需要掌握的知识:指针、结构体、动态内存管理

如果你接触过一些简单的项目,比如之前我已经提及的 “三子棋”? “扫雷” 。或者你做过类似的项目,你会发现对于一个项目的实现,主要是功能的实现,也就是所谓的接口函数。而对于之前一开头就写菜单的这种方法,我个人觉得不是特别妥当,其重心还是应该放在函数的实现上,最后才考虑菜单的实现,因为菜单只是把这些函数有逻辑的调用罢了,这一点我还是比较希望能和你产生共鸣。

目录

CONTACT.H

CONTACT.C

1.InitContact

2.AddContact

3.DelContact

4.PrintContact

5.SearchContact

6.ChangeContact

7.Exit


老样子,我们还是需要将整个程序的实现分为三个大的部分:

1.main.c:负责函数的调用以及和用户的交互

2.contact.c:负责函数的定义

3.contact.h:负责函数的声明和头文件的引用以及结构体的定义

一个通讯录里,首先我们要存放的肯定是人的信息,所以我们要想想对于一个人来讲,有哪些信息?姓名,性别,年龄,电话......数不胜数,那我们就先拿这四个举例。那么就这些而论:姓名、性别、电话,都应该是字符串类型。对于年龄可以采用整型。这样我们就要定义一个结构体把它们给 “包” 起来,方便我们管理。

CONTACT.H

对于所声明的结构体,在 main.c 以及 contact.c 里面都要使用,无疑我们肯定要定义到 contact.h? 中。如下所示:

sturct ContactMember
{
    char name[20];
    char sex[10];
    int age;
    char tel[12];
}

当然,为了命名使用的方便,我们可以 typedef:

typedef struct ContactMember CM;

就全局来看,头文件起码要有一个 stdio.h stdlib.h,先引用他们:

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

想想,如果我们要定义的是一个静态的通讯录,那我们需要定一个结构体数组,然后设定大小。如果我们又涉及到对其增删查改,那肯定要有一个能记录此时位置的地方,所以我们还是要整合一个结构体来包含它们。故如果是静态的,起码应该这样定义:

struct Contact
{
    CM data[1000];
    int count;
}

但是毕竟我们需要开辟的是动态空间,把这么大的空间放在栈区实在是有点zz,所以若是我们要把数据放在里,那肯定要用到动态内存分配,那上面的 data 我们肯定要改成指针用来接收 malloc 开辟空间的首地址, count 还是不变,想想,还缺什么?我怎么知道我开辟的空间什么时候用完呀?所以每当我们开辟一个空间,必须要有一个变量来记录此刻变量大小,所以最后的定义变成了这样:

struct Contact
{
    CM* data;
    int count;
    int capacity;
};

?当然,typedef 是必须的(我个人喜欢单独拿出来typedef,当然你也可以直接在结构体上处理)

typedef struct Contact Con;

这些解决了,是时候开始实现功能了。

最基本的:增、删、查、改。

其次还有:初始化、退出、打印等等。

当然如果你感兴趣,可以加上个排序。

所以我们对于增删查改的声明可以先放出来:

void InitContact(Con*); //初始化
void AddContact(Con*); //增
void DelContact(Con*); //删
void SearchContact(Con*); //查
void ChangeContact(Con*); //改
void PrintContact(Con*); //打印

可能你在疑惑我为什么只传了 Con*,比如删除哪个?查找哪个?这些我们可以放到函数里面 scanf 接收数据,也可以在外面接收数据传进去,那何必多用一个栈空间呢,不如就在函数里实现,还可以让代码看起来更规范化。

这样来看,contact.h 的内容差不多已经结束了,让我们进入下一板块吧。

CONTACT.C

1.InitContact

初始化,指针给NULL,其余给0即可,不过多赘述。

当然,记得把 #include "contact.h" 挂在前面

void InitContact(Con* c)
{
	c->data = NULL;
	c->count = 0;
	c->capacity = 0;
}

2.AddContact

这个得称得上是重头戏,毕竟增加数据我们需要判断是否增容,而动态内存开辟,正是我们动态通讯录的关键。而开辟的前提是什么?是读取到上限的时候。什么时候是读取上限?capacity 记录了最大容量,count 记录了当前位置,如果 count 等于了 capacity 那不就是满了嘛!这在两者同时为零时仍然适用。

    if (c->count == c->capacity)
	{
		c->capacity += 5;
		temp = (CM*)realloc(c->data,sizeof(CM)*(c->capacity));
	}

大家应该还记得我之前把c->data初始化为 NULL,这时 realloc 的魅力就随之体现,当 realloc 的第一个参数为 NULL 时其功能相当于 malloc,这样我们就用 realloc 一次解决了初次开辟和再次开辟的问题。且我用 temp 指针先接收了 realloc 返回的指针,想想是为什么?

对的,如果开辟失败,那么 realloc 就会返回 NULL,这个时候我们要对 temp 加以判断再赋值给 c->data,这里我们作为一个严厉的父亲,直接用 assert 断言,记得加头文件哦!

    if (c->count == c->capacity)
	{
		c->capacity += 5;
		temp = (CM*)realloc(c->data,sizeof(CM)*(c->capacity));

		if (temp == NULL)
		{
			assert(temp);
		}
		else
		{
			c->data = temp;
			printf("Capacity increase succeeded!\n");
		}
	}

当然,这里增容成功的提示在使用中是没必要的,但是调试阶段还是很重要的。

以上步骤都执行完毕,才到了添加的时候,但是说白了,不就是 4 个 printf 提示用户输入然后用四个 scanf 接收嘛,这里实在是没有什么好讲的,具体如下:

	printf("请输入姓名:");
	scanf("%s", c->data[c->count].name);
	printf("请输入性别:");
	scanf("%s", c->data[c->count].sex);
	printf("请输入年龄:");
	scanf("%d", &(c->data[c->count].age));
	printf("请输入电话:");
	scanf("%s", c->data[c->count].tel);

? 当然,这里拿姓名举例:c->data[c->count].name 不要绕晕了。我们这里 c 是接收的地址,是 Con* 类型的,Con* 里面有三个东西:data,count,capacity。数据存放在 data 中,所以我们指向的是 data,data 中又有 4 个数据:name,sex,age,tel。因为这里我们是以数组的方式读取,所以后面直接用点操作符了,因为数组方式读取到的是值不是地址。

还有就是唯一取地址的年龄,因为其它三个都是数组,数组名就是首元素地址,而年龄是整型,这里不要忘了取地址!添加一个联系人后 count 也要加上 1 哦!

3.DelContact

我们应该如何删除一个联系人?需要把里面的数据清零吗?删除一个联系人,我们不确定用户要删除的是哪一个,如果是最后一个还好说,但是如果是第一个呢,中间的呢?为了保证空间的连续,我们往往需要 ”牵一发而动全身“。例如我删除第一个联系人,那就需要再把数组整个向前移动 1,所以我们发现对于第一个联系人的信息没有删除的必要,因为它会被覆盖。

这里我们要求用户输入需要删除的联系人标号(也就是第几个):

	int flag = 0;
	printf("请输入您要删除的联系人标号:");
	scanf("%d", &flag);

当然,既然是用户输入,我们肯定要判断 flag 的合法性:

	if (flag <= 0 || flag > c->count)
	{
		printf("输入错误!\n");
		return;
	}

这里再次注意,用户输入的是标号,是第几个,而不是下标!下标应该是:flag - 1

故这里我们需要让整个数组从 flag 到 c->count 的位置全部向前移动 1,这时候又有一个问题出现:从前往后移,还是从后往前移?我这里举一个小小的例子,看完你就明白了:

假设这里要删除的是 3 这个元素,我要把 4 和 5 整体前移 1?

?这时候 c->count--; 其最后一个 5 也就相当于失效了,这里我们删除 3 的目的已经达到,从前往后可知是完全可行的,那么从后往前呢?

全变成了 5 ?由此不难得出,从后往前是行不通的。所以我们开始遍历从前往后覆盖

首先我想给大家先看看循环:

	for (int i = 0; i < c->count - flag; i++)
	{

	}

?我们移动就需要知道移动几次,就如上面的例子,我们删除 3,总个数是 5,我们移动了 2 次,不难推到其次数就是:c->count - flag;

但是移动的时候是我们需要的是下标,所以移动操作我们是这样实现的:

	for (int i = 0; i < c->count - flag; i++)
	{
		c->data[flag - 1 + i] = c->data[flag + i];
	}
  
    c->count--;

这里我们直接对c->data进行覆盖,切忌不要写成了:

c->data[flag - 1 + i].name = c->data[flag + i].name;
c->data[flag - 1 + i].sex = c->data[flag + i].sex;
c->data[flag - 1 + i].age = c->data[flag + i].age;
c->data[flag - 1 + i].tel = c->data[flag + i].tel;

这样写多少是带点bing了~

这里还需要强调非常重要的一点,写好一个接口函数,最好就去测试一下,保证这一步没问题了我们再往下一步走。不然到时候运行出错,不知道是这个的问题还是那个的问题,这一点一定要重视!至于测试过程我就省略了,这需要你们在 main.c 里面进行测试,这也就是不写菜单的好处,测试省略了很多不必要的麻烦。

4.PrintContact

其实这个函数按道理我们应该最先写出来的,因为打印出通讯录可以帮助我们更直观的看到通讯录的状态,方便我们测试,打印的个数自然是取决于 c->count 了,这里循环即可!

	for (int i = 0; i < c->count; i++)
	{
		printf("-----------------\n");
		printf("标号:%d\n", i + 1);
		printf("姓名:%s\n", c->data[i].name);
		printf("性别:%s\n", c->data[i].sex);
		printf("年龄:%d\n", c->data[i].age);
		printf("电话:%s\n", c->data[i].tel);
		printf("-----------------\n");
	}

5.SearchContact

其实查找函数如果细细的想,应该是需要返回值的,如果查找成功了就返回下标,如果失败了就返回-1。当然,如果你硬是把打印函数也套在查找函数里,当然可行,但是此函数的通用性就差了。比如别人就是要根据这个返回值干啥干啥的,你就直接帮别人打印出来了,别人没有处理空间。所以这里我们还是要在头文件里修改一下:

int SearchContact(Con*);

既然是搜索,那我们肯定要根据一个玩意儿搜索,比如姓名。(你也可以让用户自行决定,这里演示需要我考虑太复杂的情况)

	char input[20];
	printf("请输入您要查找的联系人姓名:");
	scanf("%s", &input);

	for (int i = 0; i < c->count; i++)
	{
		if()
	}

写到这里我突然想问大家,姓名是字符串,我们如何判断字符串相等?

如果你真的这样写了:

if(input == c->data[i].name)

呃,大聪明你好。这样比较的是什么,好好想想。难不成还指望它们的地址相等哦?所以这里头文件里又要填上一项:

#include <string.h>

这里我们要用字符串比较函数来判断字符串是否相等:

int SearchContact(Con* c)
{
	char input[20];
	printf("请输入您要查找的联系人姓名:");
	scanf("%s", &input);

	for (int i = 0; i < c->count; i++)
	{
		if (strcmp(input, c->data[i].name) == 0)
		{
			return i;
		}
	}
	return -1;
}

当然,如果你要单独打印一个人的信息,可能又要多出一个函数来打印指定人的信息,不过代码已经是大同小异。

6.ChangeContact

如何修改一个联系人信息?说白了就是再录入一遍覆盖就好了,这里我们就需要用户输入标号来定位一下具体联系人:

	int flag = 0;
	printf("请输入需要修改的联系人标号:");
	scanf("%d", &flag);

当然,判断合法性也是必要步骤:

	if (flag <= 0 || flag > c->count)
	{
		printf("输入错误!\n");
		return;
	}

接下来就是简单的覆盖了:

	printf("请输入姓名:");
	scanf("%s", c->data[flag - 1].name);
	printf("请输入性别:");
	scanf("%s", c->data[flag - 1].sex);
	printf("请输入年龄:");
	scanf("%d", &(c->data[flag - 1].age));
	printf("请输入电话:");
	scanf("%s", c->data[flag - 1].tel);

这里还需要注意的一点就是 flag 是标号而不是下标,下标还是要用 flag - 1;

7.Exit

我并没有单独写成函数,而是直接在 main.c 中实现。既然是动态内存开辟的空间,那么我们就要遵循三原则:开辟,检查,释放。现在空间使用完毕,我们要做如下步骤:

	free(c.data);
	c.data = NULL;

接下来,如果你想写个菜单,当然没问题。这个得交给你自己了。

就本程序而言,接口函数负责实现功能是重中之重,而菜单只是为了和用户交互,只是负责调用函数的逻辑顺序等,过于简单,如有疑问可移步:

三子棋,其有关于菜单的详细讲解

本次代码上传至码云:通讯录 - 博客搭建

如有任何疑问、错误、表达不当等处请及时评论留言,本人发现会及时修改和回答。

END

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 10:54:41  更:2022-09-13 10:57:37 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:15:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码