C语言用结构体实现静态通讯录

前言:

这几天学到了数据结构的顺序表,就回头想把结构体的内容整理一下,和大家一起分享我写静态通讯录的一些心得和感受,交流一下学习成果。

实现通讯录思路:

1.建立分组

我们需要头文件(contact.h)来进行函数的声明,需要有进行测试的源文件(test.c)和实现函数的源文件(contact.c)。

创立分组的好处:

  1. 提高可维护性:将功能模块的定义(声明)和实现分开,可以使代码更加清晰和易于理解。头文件通常包含函数声明、结构体定义、常量等,可以提供对外的接口和抽象数据类型定义。源文件包含函数的具体实现和数据结构的操作等。这种分组方式有助于开发人员更好地组织和维护代码,便于阅读和修改。当需要调试或修改某个模块时,只需要关注源文件,而无需查看整个项目的头文件。

  2. 减少编译时间:头文件包含了各种声明和定义,而实现文件只包含具体实现。在编译源文件时,编译器只需要包含需要的头文件,而不需要每次都重新编译整个项目。这样可以减少不必要的编译时间,提高编译效率。

  3. 解决循环引用问题:在一个较复杂的项目中,可能会存在多个模块相互依赖的情况。如果将所有的定义和声明都放在一个文件中,可能会导致出现循环引用的问题,导致编译错误。通过将声明放在头文件中,并使用前向声明(forward declaration)来解决循环引用问题。

  4. 接口隔离和信息隐藏:头文件为用户提供了对外的接口,并隐藏了实现的细节。这样,用户只需要关心如何使用接口,而无需关心实现的细节。这有利于降低代码的耦合度,提高代码模块的独立性和可重用性。

  5. 提高编译器的优化能力:将函数的实现放在源文件中,编译器可以更充分地进行代码优化,提高程序的执行效率。编译器可以根据实际的调用情况进行内联展开、函数内寄存器分配等优化。

#define _CRT_SECURE_NO_WARNINGS
#define MAX 100  //最多存放100人的信息
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
//函数的声明

//人的信息
#pragma once  //确保头文件在源文件里不被重复编译
typedef struct PeoInfo
{
	char name[20];
	int age;
	char sex[10];
	char tele[12];
	char addr[30];
}PeoInfo;

//通讯录
typedef struct Contact 
{
	PeoInfo data[MAX];//存放人的信息
	int count;//记录当前通讯录实际人数
}Contact;

注:typedef(关键字)的作用

1.简化复杂的类型声明:使用 typedef 可以将复杂且冗长的类型声明替换为一个简单的别名,使代码更容易理解和阅读。

若一个数据类型声明为long long,可以直接替换成uint_t表示数据名称。

typedef long long uint_t;

2.提高代码的可维护性:通过为类型定义有描述性的别名,可以使代码更具可读性和可维护性。类型别名可以更好第反映变量的含义和用途,减少代码中的歧义和错误。

typedef struct {
    int x;
    int y;
} Point;
Point p; // 替代了 struct Point p;

d32a571faa14461c94607c6b5cac011f.png

3.跨平台兼容性:使用typedef可以帮助解决不同平台和编译器之间的类型差异问题。通过为特定类型定义别名,并在不同平台上使用相应别名,能够确保代码在不同环境中的正确性和可移植性。

万事俱备,只欠东风,我们需要声明一个结构体变量,那我们怎么来表示结构体变量呢?

 首先我们要明确一点,我们要从通讯录中去找数据,所以我们一定是创建表示通讯录结构体的变量,因为我们把联系人的信息放到data这个PeoInfo结构体变量中,data又在Contact这个结构体里,那我们只创建一个Contact结构体变量,不仅可以调用Contact结构体里的变量成员,还可以调用PeoInfo结构体里的变量成员。

那我们为什么怎么做呢?待会你就知道了。

2.分装函数

确定我们通讯录的功能,要有添加、删除、查找、修改、显示、排序联系人信息等基本功能,

那我们就分装相对应的函数来实现我们的功能。

注:将自定义的函数放到头文件进行声明,这样我们可以直接在源文件中直截调用这些分装好的函数。

//初始化通讯录
void InitContact(Contact* pc);
//增加联系人
void AddContact(Contact* pc);
//打印通讯录中的信息
void ShowContact(const Contact* pc);
//删除指定联系人
void DelContact(Contact* pc);
//查找指定联系人
void SearchContact(Contact* pc);
//修改指定联系人
void ModifyContact(Contact* pc);
//排序通讯录中的内容
void SortContact(Contact* pc);

大家注意到了吗?我这里创建的结构体变量是一个结构体指针变量,而不是结构体变量,为什么呢?

  1. 内存动态分配:使用结构体指针变量可以在运行时动态地分配内存空间,而结构体变量在编译时需要确定其大小。这使得结构体指针变量更加灵活,可以根据实际需要动态调整内存大小。

  2. 内存共享和传递:通过传递结构体指针作为参数,可以在函数之间共享和访问同一份结构体数据,而不需要进行复制。这节省了内存开销,提高了程序的性能和效率。

  3. 修改原始数据:使用结构体指针变量,可以直接修改原始数据,而不需要进行拷贝操作。这对于涉及大型结构体或需要频繁修改数据的情况下,可以减少不必要的复制操作和内存开销。

  4. 关联性:结构体指针变量可以将多个结构体实例关联在一起,通过指针链接形成链表、树等数据结构,以便快速遍历和操作。这在处理复杂的数据结构和对结构体进行动态整理时非常有用。

  5. 减少函数调用开销:通过传递结构体指针作为参数,可以减少函数调用时结构体的数据复制开销,从而提高了程序的性能。

这为我们后期修改静态通讯录提升空间有很大的帮助,而且大大缩短我们程序运算的时间!但需要注意的是,在使用结构体指针变量时需要确保指针指向合法的内存地址,并且注意内存的分配和释放,避免内存泄漏或野指针的出现。

注:const(关键字)的作用

用于声明一个常量,它可以用来修饰变量、函数参数、函数返回值等。它的作用是使被修饰的实体成为只读的,即它们的值在声明后就不能再被修改。

1.在变量声明中,const 位于类型修饰符之前,用来修饰变量,表示该变量的值不能被修改。

例如:

const int a = 100;

上面的代码中,a 被声明为一个不可变的常量,其值为 100,不能再被修改。 

2.在函数参数声明中,const 用于修饰函数形参,表示函数内部不会修改这个参数的值。

例如:

void print(const char* p);

上面的代码中,p是一个指向字符的指针,被声明为 const char* 类型,表示在 print 函数内部,不会修改指向的字符内容。 

3.在函数返回值声明中,const 用于修饰函数返回值,表示返回的值是一个常量,不能被修改。

例如:

const float PI = 3.14159f;

const float getPI() {
    return PI;
}

上面的代码中,getPi 函数返回一个常量浮点数,表示返回的值不能被修改。 

总而言之,const 是用来声明常量或修饰不可修改的实体。它在编程中经常用于增加代码的可读性和可靠性,并可以提供一定的安全性保证。

那我们这里的添加const的自定义函数的功能是用来打印静态通讯录中的信息的,要确保Contact结构体指针变量指向的内容不被修改,才能保证我们读取到的信息是真实有效的。

3.用户界面的创建

还记得我们建立的分组吗,我们要在test.c里创建用户菜单,给用户进行选择。

#define _CRT_SECURE_NO_WARNINGS

#include"contact.h"
void menu()
{
	printf("********************************\n");
	printf("******1. add     2. del   ******\n");
	printf("******3. search  4. modify******\n");
	printf("******5. show    6. sort  ******\n");
	printf("******0. exit             ******\n");
	printf("********************************\n");
}

这个菜单把我们的通讯录的功能给展现出来,但用户还不能做出选择,因为我们没有实现对数据的写入功能。

int main()
{
	int input = 0;
	Contact con;//通讯录
	//初始化通讯录
	InitContact(&con);

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			AddContact(&con);
			break;
		case 2:
			DelContact(&con);
			break;
		case 3:
			SearchContact(&con);
			break;
		case 4:
			ModifyContact(&con);
			break;
		case 5:
			ShowContact(&con);
			break;
		case 6:
			SortContact(&con);
			break;
		case 0:
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误");
			break;
		}
	} while (input);
	return 0;
}

看起来有点长哈,其实道理还是很简单的,利用switch语句和do while语句构建一个选择分支循环。

struct Contact* pc; // 定义指向结构体的指针变量 pc
struct Contact con; // 定义结构体变量 con
pc = &con; // 将结构体变量 con 的地址赋值给指针变量 pc

这也是为什么我们使用结构体指针变量的原因,使用指针变量可以提高程序的效率和灵活性,同时也方便了对结构体的访问和修改 。

 但还是感觉哪里怪怪的,感觉有个地方可以再优化一下,是哪里呢?

注:enum(关键字)的基本用法

我们可以用enum(关键字)来表示我们通讯录上的功能,那具体怎么使用呢?

//先定义类型
enum Opition //类型名称就是enum Opition 
{
    EXIT,    //从0开始依次增加
    ADD,     // 1
    DEL,     // 2
    SEARCH,  // 3
    MODIFY,  // 4
    SHOW,    // 5
    SORT     // 6
};

        这样子我们就可以用枚举常量来表示数字了 ,枚举是C语言中常见的一种基本数据类型,他可以让我们的数据更简洁,更易读。在之后有关宏定义的相关学习中,我们还会深入了解枚举的用法,这里就不多做扩展。让我们修改一下我们的代码吧!

int main()
{
	int input = 0;
	Contact con;//通讯录
	//初始化通讯录
	InitContact(&con);

    
    //先定义类型
    enum Opition //类型名称就是enum Opition 
    {
        EXIT,    //从0开始依次增加
        ADD,     // 1
        DEL,     // 2
        SEARCH,  // 3
        MODIFY,  // 4
        SHOW,    // 5
        SORT     // 6
    };
	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 SHOW:
			ShowContact(&con);
			break;
		case SORT:
			SortContact(&con);
			break;
		case EXIT:
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误");
			break;
		}
	} while (input);
	return 0;
}

是不是看着简洁明了多了!那现在我们已经完成用户页面了,接下来就是把每个函数的算法逻辑搞定,就可以调用函数完成我们程序的功能了!

4.构建自定义函数

1.初始化通讯录的函数

void InitContact(Contact* pc)
{
	pc->count = 0;
	memset(pc->data, 0, sizeof(pc->data));
}

可以看到我们这个自定义函数用了一个很巧妙的方法,用我们的结构体指针变量指向单个数据内存当中,用了库函数里的memset函数,初始化通讯录里的信息全为0。

用来记录存放联系人数量的count结构体成员,也初始化为0,意味着我们一开始联系人的数量为0。

注:memset函数的基本用法 

当需要将一块内存区域中的数据设置为指定的值时,可以使用 memset 函数。

memset 函数的原型如下:

void* memset(void* ptr, int value, size_t num);

参数说明:

ptr:指向要设置的内存区域的指针。

value:要设置的值,通常为 unsigned char 类型。

num:要设置为指定值的字节数。

memset 函数将 ptr 指向的内存区域中的每个字节都设置为 value 参数指定的值,设置的字节数由 num 参数指定。函数返回的是 ptr

以下是 memset 的一个示例用法:

#include <string.h>

int main()
{
    char str[10] = "Hello";
    memset(str, 'A', 5);
    printf("%s\n", str);  // 输出 "AAAAA"

    return 0;
}

在这个示例中,我们定义了一个字符数组 str,并用初始值 “Hello” 初始化了前 5 个元素。然后,我们使用 memset 函数将数组 str 中的前 5 个字节都设置为字符 ‘A’,即 'A'

最终,我们输出数组 str,可以看到输出结果为 “AAAAA”,说明 memset 函数成功将数组中的部分元素值设为指定值。

需要注意的是,memset 函数是以字节为单位进行设置的,因此对于非字符类型的数组,可以将要设置的值转换为对应的字节形式。

好了,现在我们已经了解了memset函数的具体用法了,那我们速速开始编写下一个自定义函数吧!

2.增加通讯录联系人的函数

void AddContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	if (pc->count == MAX)
	{
		printf("通讯录已满,无法添加\n");
		return;
	}
	
	printf("请输入名字:>");
	scanf("%s", pc->data[pc->count].name);
	printf("请输入年龄:>");
	scanf("%d",&pc->data[pc->count].age);
	printf("请输入性别:>");
	scanf("%s", pc->data[pc->count].sex);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->count].tele);
	printf("请输入地址:>");
	scanf("%s", pc->data[pc->count].addr);

	pc->count++;
	printf("增加成功\n");
}

这串代码就十分简单易懂了,唯一需要关注的是我们要用一个assert宏来避免指针pc为空指针,导致程序发生问题,所以用assert宏来进行真假判断。

:assert宏的基本用法

  1. 引入头文件:在使用 assert 之前,需要引入 <assert.h> 头文件。

  2. 断言条件:assert 宏接受一个表达式作为参数,用于检查某个条件是否为真。这个条件通常是一个逻辑表达式,用于测试程序中的假设或预期行为。

  3. 断言失败的处理:若断言条件为假,即表达式的值为0,则 assert 失败。此时,程序终止并输出一条错误消息,通常会包含断言失败的表达式。错误消息会被发送到标准错误输出流。

一般来说,assert 是用于在开发过程中进行调试和错误检查的工具。它可用于:

检查函数或程序中的输入参数是否满足特定条件。

验证函数或程序中的逻辑假设和过程是否正确。

检测潜在的错误或程序错误的发生点,并提供相应的错误提示信息。

需要注意的是,断言应该用于检查在正常情况下应该成立的条件。不能将断言作为处理错误和异常的机制,而应该使用其他适当的错误处理机制。

大家可以自己去尝试一下写一个错误程序,看看报错情况,这里就不做过多讲解了。跟上我的步伐去编写下一个自定义函数吧!

3.打印通讯录信息的函数

void ShowContact(const Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	int i = 0;
	printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址");
	for (i = 0; i < pc->count; i++)
	{
		printf("%-20s\t%-5d\t%-5s\t%-12s\t%-30s\n", pc->data[i].name,
			pc->data[i].age,
			pc->data[i].sex,
			pc->data[i].tele,
			pc->data[i].addr);
	}
}

这个函数的实现也是非常的常规,没有什么难度,但有一个小细节不知道大家有没有注意到,那就是我们的 占位符为啥前面有个负号,莫不是有问题?

非也非也,这是表示输出一个向左对齐的字符串,数字代表的是字符串里的字符总宽度(虽然我知道大家都是天才,这种没必要讲,但保持负责的态度还是要讲解清楚)。

\t 是制表符的转义序列,它在输出中具有一定的作用,可以在文本中插入水平制表符。制表符可以在输出中创建一列等宽的空白,跳到下一个制表位。它一般会跳到下一个 Tab 停止位,使得输出对齐更整齐。我们经常在编辑器、表格和代码中使用制表符来进行排列和对齐。在输出中使用制表符也有类似的效果。

在上面提到的代码示例中,制表符 \t 在格式化字符串中的使用可以使得输出表头的各个列之间保持了固定的间距,使表格更加整齐对齐。

例如,如果没有制表符,输出可能如下所示:

名字  年龄  性别  电话     地址

 使用了制表符之后,输出变得像下面这样,每列之间都有固定的间距,使得表头更加整齐对齐:

名字                年龄   性别   电话          地址

是不是感觉非常完美,对于那些强迫症重度患者来说就是必不可少的绝招!但需要注意的是,由于制表符的宽度是固定的,对于不同字体和输出环境可能会产生不同的效果,所以有时可能需要使用其他方法来实现更灵活的格式化。 我们这里就不多做延伸了,还有好几个函数正在等着我们去实现。

4.删除指定联系人的函数

void DelContact(Contact* pc)
{
	char name[20] = { 0 };
	assert(pc); //避免传入空指针导致发生程序错误
	int i = 0;
	if (pc->count == 0)
	{
		printf("通讯录为空,没有信息可以删除\n");
		return;
	}
	printf("请输入要删除人的名字:>");
	scanf("%s", name);

	//1.查找
	int pos = FindByName(pc, name); //查找联系人名字是否正确
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
    //2.删除 
	for (i = pos; i < pc->count-1; i++)
	{
		pc->data[i] = pc->data[i + 1];
	}
	pc->count--;
	printf("删除成功\n");
}

 这里需要强调的知识很多,我们来仔细看看代码。

我们需要考虑到如果没有联系人的信息,那还能进行删除吗,那肯定不能,所以为了避免我们的程序出现问题,我们要设置条件,如果联系人数量为0,那我们就不再进行下面的操作,返回菜单页面。

if (pc->count == 0)
	{
		printf("通讯录为空,没有信息可以删除\n");
		return;
	}

那我们已经有了存放好的联系人信息,那我们就可以指定进行删除的操作啦,但是我们计算机要找到我们每个人的信息并不是无师自通,同样也需要我们的引导,我们联系人信息都存放在结构体内存当中,一个萝卜一个坑,我们需要确定我们要找的萝卜在不在这个坑里面,万一找错了萝卜那不就搞错了吗?所以我们要核实输入的联系人名字和内存地址上储存的联系人名字是否为同一个名字,这样我们才能进行删除的操作,不能我们就返回菜单页面。

当然我们注意到一个很严重的问题,咋去确认我们输入的联系人与我们存放的联系人所对应信息是一致的? 我们就再再再创建一个函数来查找我们的联系人信息与输入联系人信息进行比较,如果字符一致,那就给出查找联系人在内存地址中的位置,再来进行删除,长话短说,咱们直接实践走起!

构建查找联系人信息相应位置的函数

static int FindByName(Contact* pc, char name[])
{
	assert(pc); //避免传入空指针导致发生程序错误
	int i = 0;
	for (i = 0; i < pc->count; i++)
	{
		if (0 == strcmp(pc->data[i].name, name))
		{
			return i;
		}
	}
	return -1;
}

 现在我们把我们的查找联系人信息存放位置的函数给编写出来了,这里有几个值得注意的知识点要掌握!

注:strcmp函数的基本用法

比较两个字符串的内容是否相同,并返回一个整数作为比较结果需要使用strcmp函数。

strcmp 函数的基本语法是:

int strcmp(const char* str1, const char* str2);

其中 str1 和 str2 是要比较的两个字符串的指针。

strcmp 函数的返回值有以下可能:

如果 str1 和 str2 的内容相同,则返回值为 0。

如果 str1 的内容在字典顺序上比 str2 的内容小,则返回值为一个负整数。

如果 str1 的内容在字典顺序上比 str2 的内容大,则返回值为一个正整数。

具体来说,返回的整数值是 str1 和 str2 第一个不匹配字符的 ASCII 码之差。因此,如果 strcmp 函数返回值为零,表示两个字符串相等;如果返回值小于零,表示 str1 小于 str2;如果返回值大于零,表示 str1 大于 str2

下面是一个使用 strcmp 函数的示例:

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

int main() {
    char str1[] = "apple";
    char str2[] = "banana";

    int result = strcmp(str1, str2);

    if (result == 0) {
        printf("str1 和 str2 相等\n");
    } else if (result < 0) {
        printf("str1 小于 str2\n");
    } else {
        printf("str1 大于 str2\n");
    }

    return 0;
}

950ff8ae57f04ba18cef6ac27f87ad29.png

b和a差一个ASCII 码值,得出的整数为-1,结果为我们上图的情况。

好了,现在我们已经搞定了我们的联系人信息与输入联系人信息如何进行比较的问题,我们需要进行代码实现,那我们就用一个循环语句将我们输入的联系人名字和内存中一个一个进行比较(虽然很傻,但真的很有效),找到就返回我们我们联系人的内存位置,找不到就结束查找函数,进入下一个环节,将我们的返回值取为-1,如果信息人位置为-1,那就告诉用户不存在该联系人(真的太人性化了)。

int pos = FindByName(pc, name); //查找联系人名字是否正确
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}

 3d27317449ad4dc49618e9f37c2bd8d7.png

 大家也肯定注意到了,我们这个自定义的函数前面加了一个static,那这个具体是用来干嘛的呢?

注:static(关键字)的具体用法

  1. 修饰局部变量:

    • 在函数内部使用 static 修饰的局部变量具有静态生存期。这意味着该变量在函数第一次被调用时初始化,并保留其值,直到程序结束。它在多次调用函数期间保持不变。
    • static 修饰的局部变量的作用域仅限于定义它的块(如函数或代码块)。这意味着其他函数无法访问该变量。
    • static 修饰的局部变量默认具有内部链接,这意味着它仅在定义它的文件中可见。
  2. 修饰全局变量:

    • 使用 static 修饰的全局变量具有内部链接。它在定义它的文件中可见,但在其他文件中不可见。
    • static 修饰的全局变量仅在程序的生命周期内存在,不同于没有 static 修饰的全局变量,其生命周期为整个程序运行时间。
  3. 修饰函数:

    • 使用 static 修饰的函数具有内部链接。它们只在定义它们的文件内可见,无法被其他文件调用。
    • 这种用法可以用于实现文件内私有的辅助函数,限制其它文件的访问。
  4. 修饰数据结构成员:

    • 在数据结构中使用 static 修饰的成员变量具有与结构相关的特性,而不是与结构的每个实例相关的属性。即所有结构实例共享同一份数据,而不是每个实例都有自己的数据。
    • 这种用法可以用于在数据结构中包含静态共享数据。

我们这里就是保证我们这个自定义查找函数仅限于该文件,也就是实现我们函数的源文件,不会被其他文件所进行访问,这样我们就可以保证我们这段代码的实现仅在这个源文件当中,不会造成干扰和冲突。

终于到了最后一步了,当我们找到我们要找的联系人后,我们要对内存地址进行修改,那该怎么做呢?不如把要删除联系人的信息进行覆盖,那开动我们的小脑筋想想该怎么实现呢?我希望下面的图解能帮助你快速找到秘诀。

2e4b686b401b409ea2dd268480ea2cb9.png

假如这是我们存放联系人信息的内存地址,我想把3这个位置的数据清除,我们需要进行数据覆盖,就如下图所示:

dcc51f1354ad4db8afed461734c8caef.png

现在我们把3位置上的数据覆盖成4位置上的数据了,那只需要我们把原本4位置上的数据覆盖成5位置上的数据,以此类推,就如下图所示:

3419822e6712455a90a5d43bbfdcd384.png

在我们第7个位置没有联系人的数据,所以会覆盖成初始状态。

for (i = pos; i < pc->count-1; i++)
	{
		pc->data[i] = pc->data[i + 1];
	}
	pc->count--;

那我们就可以根据这个思路来实现我们自定义函数的功能了,从我们要覆盖的联系人位置开始,用循环语句进行一个又一个数据覆盖,别忘记把我们联系人的数量减少一个,这样就完成了我们的删除功能了! 

5.查找指定联系人的函数

void SearchContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	char name[20] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);
	//1.查找	
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	//2.打印
	printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址");
		printf("%-20s\t%-5d\t%-5s\t%-12s\t%-30s\n", pc->data[pos].name,
			pc->data[pos].age,
			pc->data[pos].sex,
			pc->data[pos].tele,
			pc->data[pos].addr);
}

这一步的函数实现相对来说较简单,因为我们可以用我们的已经创建好的自定义函数进行联系人位置上的查询,要是信息正确,那我们就可以直接打印出这个内存地址存放的数据信息。

6.修改指定联系人信息的函数

void ModifyContact(Contact* pc)
{
	assert(pc);
	char name[20] = { 0 }; //避免传入空指针导致发生程序错误
	printf("请输入要查找人的名字:>");
	scanf("%s", name);
	//1.查找	
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	printf("要修改的人信息已经查找到,接下来开始修改!\n");
	//2.修改
	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].tele);
	printf("请输入地址:>");
	scanf("%s", pc->data[pos].addr);

	printf("修改成功!\n");
}

不知不觉已经到了倒数第二个自定义函数了,不知道大家还记得多少知识?

这个自定义函数的实现同样也比较简单,我们只需要搞清楚指定联系人的位置在哪里,我们在原来的内存地址上直接就可以修改数据了。

这里有一个细节不知道大家有没有注意到:

7f80ab59c85541e482f188424e62a8ea.png

整型数据的写入我加了取地址操作符,而字符串类型的数据我并没有加,但仍然能够让程序运行这是为什么呢?

这是因为,在大多数情况下,字符串字面量被视为指向该字符串的第一个字符的指针。当你将字符串字面量传递给需要指针的函数或变量时,编译器会自动将其转换为指向字符串字面量的指针,也就意味着会从第一个元素一直读取到最后。

需要注意的是,尽管字符串字面量可以被隐式转换为指针,但请确保在对字符串进行修改或需要指针操作时,使用字符数组或动态分配的内存来存储字符串。

7.将通讯录里的联系人进行排序的函数

int cmp_peo_by_name(const void* e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->name,((PeoInfo*)e2)->name);
}

//按照名字来排序
void SortContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	qsort(pc->data, pc->count, sizeof(PeoInfo), cmp_peo_by_name);
	printf("排序成功\n");
}

我们这个自定义函数的排序方法我采用的是看联系人名字首字母的由小到大以此存放在结构体内存当中 ,那如何来实现我的想法呢,我这里使用了一个快速排序的方法,也就是使用qsort函数来帮我解决这个问题。

注:qsort函数的基本用法

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

以下是对这些参数的解释:

  1. base:要排序的数组或容器的基准指针,指向要排序的第一个元素。

  2. nmemb:数组或容器中元素的数量。

  3. size:每个元素的大小(以字节为单位)。

  4. compar:比较函数的指针。比较函数用于确定元素之间的顺序。它接受两个指向元素的指针,并返回一个整数值,表示两个元素的相对顺序。

 比较函数的原型应为:

int compar(const void* a, const void* b);

也就是我们这里的

int cmp_peo_by_name(const void* e1, const void* e2)
int cmp_peo_by_name(const void* e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->name,((PeoInfo*)e2)->name);
}

我已经定义了一个名为cmp_peo_by_name的比较函数,该函数用于按照联系人的名字排序。cmp_peo_by_name函数的参数类型是const void*,这是因为qsort函数要求比较函数的参数类型为 const void*。在函数体中,我使用了strcmp函数来比较两个联系人的名字。

//按照名字来排序
void SortContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	qsort(pc->data, pc->count, sizeof(PeoInfo), cmp_peo_by_name);
	printf("排序成功\n");
}

我的SortContact函数将调用qsort函数来对联系人列表进行排序。

我将排序的基准指针传递给pc->data,pc->count指定了要排序的元素首位和元素数量。

sizeof(PeoInfo)指定了每个元素的大小,而 cmp_peo_by_name则指定了比较函数。

排序会根据联系人的名字进行升序排序。排序过程中会根据比较函数的返回值进行元素位置的交换。

当比较函数的返回值为负数(return void* e1 - void* e2)时,qsort函数会将被比较的元素按照所定义的比较规则进行升序排序。换句话说,被认为较小的元素会排在被认为较大的元素之前。反之亦然,当比较函数的返回值为正数(return void* e2 - void* e1)时,qsort函数会将被比较的元素按照所定义的比较规则进行降序排序。换句话说,被认为较大的元素会排在被认为较小的元素之前。

 需要注意的是,我们实现这个自定义函数需要包含标准库的头文件 stdlib.h 来使用 qsort 函数。

5.完成代码编写

现在我们可以把我们实现函数的源文件(contact.c)和进行测试的源文件(test.c)放在头文件下:

头文件contact.h:

#define _CRT_SECURE_NO_WARNINGS
#define MAX 100  //最多存放100人的信息
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
//函数的声明

//人的信息
#pragma once  //确保头文件在源文件里不被重复编译
typedef struct PeoInfo
{
	char name[20];
	int age;
	char sex[10];
	char tele[12];
	char addr[30];
}PeoInfo;

//通讯录
typedef struct Contact 
{
	PeoInfo data[MAX];//存放人的信息
	int count;//记录当前通讯录实际人数
}Contact;

实现函数源文件contact.c:

#define _CRT_SECURE_NO_WARNINGS
#include"contact.h"

void InitContact(Contact* pc)
{
	pc->count = 0;
	memset(pc->data, 0, sizeof(pc->data));
}

void AddContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	if (pc->count == MAX)
	{
		printf("通讯录已满,无法添加\n");
		return;
	}
	//
	printf("请输入名字:>");
	scanf("%s", pc->data[pc->count].name);
	printf("请输入年龄:>");
	scanf("%d",&pc->data[pc->count].age);
	printf("请输入性别:>");
	scanf("%s", pc->data[pc->count].sex);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->count].tele);
	printf("请输入地址:>");
	scanf("%s", pc->data[pc->count].addr);

	pc->count++;
	printf("增加成功\n");
}

void ShowContact(const Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	int i = 0;
	printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址");
	for (i = 0; i < pc->count; i++)
	{
		printf("%-20s\t%-5d\t%-5s\t%-12s\t%-30s\n", pc->data[i].name,
			pc->data[i].age,
			pc->data[i].sex,
			pc->data[i].tele,
			pc->data[i].addr);
	}
}

static int FindByName(Contact* pc, char name[])
{
	assert(pc); //避免传入空指针导致发生程序错误
	int i = 0;
	for (i = 0; i < pc->count; i++)
	{
		if (0 == strcmp(pc->data[i].name, name))
		{
			return i;
		}
	}
	return -1;
}

void DelContact(Contact* pc)
{
	char name[20] = { 0 };
	assert(pc); //避免传入空指针导致发生程序错误
	int i = 0;
	if (pc->count == 0)
	{
		printf("通讯录为空,没有信息可以删除\n");
		return;
	}
	printf("请输入要删除人的名字:>");
	scanf("%s", name);

	//删除
	//1.查找
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
    //2.删除 
	for (i = pos; i < pc->count-1; i++)
	{
		pc->data[i] = pc->data[i + 1];
	}
	pc->count--;
	printf("删除成功\n");
}

void SearchContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	char name[20] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);
	//1.查找	
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	//2.打印
	printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址");
		printf("%-20s\t%-5d\t%-5s\t%-12s\t%-30s\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)
{
	assert(pc); //避免传入空指针导致发生程序错误
	char name[20] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);
	//1.查找	
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	printf("要修改的人信息已经查找到,接下来开始修改!\n");
	//2.修改
	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].tele);
	printf("请输入地址:>");
	scanf("%s", pc->data[pos].addr);

	printf("修改成功!\n");
}

int cmp_peo_by_name(const void* e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->name,((PeoInfo*)e2)->name);
}

//按照名字来排序
void SortContact(Contact* pc)
{
	assert(pc); //避免传入空指针导致发生程序错误
	qsort(pc->data, pc->count, sizeof(PeoInfo), cmp_peo_by_name);
	printf("排序成功\n");
}

进行测试源文件:

#define _CRT_SECURE_NO_WARNINGS

#include"contact.h"
void menu()
{
	printf("********************************\n");
	printf("******1. add     2. del   ******\n");
	printf("******3. search  4. modify******\n");
	printf("******5. show    6. sort  ******\n");
	printf("******0. exit             ******\n");
	printf("********************************\n");
}

int main()
{
	int input = 0;
	Contact con;//通讯录
	//初始化通讯录
	InitContact(&con);

    
    //先定义类型
    enum Opition //类型名称就是enum Opition 
    {
        EXIT,    //从0开始依次增加
        ADD,     // 1
        DEL,     // 2
        SEARCH,  // 3
        MODIFY,  // 4
        SHOW,    // 5
        SORT     // 6
    };
	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 SHOW:
			ShowContact(&con);
			break;
		case SORT:
			SortContact(&con);
			break;
		case EXIT:
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误");
			break;
		}
	} while (input);
	return 0;
}

这样我们的静态通讯录就全部搞定啦,如果还想加入更多功能,那就自己尝试去做一下吧,以后还会和大家讲一下动态通讯录该如何编写,动态通讯录可以在我们静态通讯录的基础上扩大我们可输入联系人的数量,不用考虑结构体内存不足的问题,那当然是之后的事情了。

结言: 我们根据结构体的相关知识把静态通讯录做了一个详细的完成,我认为我还是讲的满详细的,希望能给大家的代码学习之旅带来更多帮助,这篇博客花了我两天的时间,身为大一的学生,确实很难抽空静下心来总结和深化自己所学的内容,但最终还是不辱使命,我也在编辑的过程中学习了很多,希望下次能给大家带来更多优质内容!

如果文章中有不对的地方欢迎指出,我都会虚心学习并改正,如果对源码有不理解的地方,也可以在评论区里询问我,我尽可能帮助解答!

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

comerun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值