当前位置: 首页 > news >正文

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)


文章目录

  • c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)
  • 前言
  • 一、结构体
    • 1. 结构的基础知识
    • 2. 结构的声明
    • 3. 特殊的声明
    • 4. 结构的自引用
    • 5. 结构体变量的定义和初始化
    • 6. 结构体内存对齐
    • 7. 修改默认对齐数
    • 8. 结构体传参
  • 二、位段
    • 1. 什么是位段
    • 2. 位段的内存分配
    • 3. 位段的跨平台问题
    • 4. 位段的应用
  • 三、枚举
    • 1. 枚举类型的定义
    • 2. 枚举的优点
    • 3. 枚举的使用
  • 四、联合
    • 1. 联合类型的定义
    • 2. 联合的特点
    • 3. 联合大小的计算
    • 4. 联合休的使用实例:
  • 五、使用结构体实现一个通讯录
  • 总结


前言

c语言中虽然定义得有许多的类型,如char、int、double等等,但是现实生活中需要描述的问题,仅仅使用这些已有类型是不够的,所以在c语言中,程序员可以根据自己的需要,定义出自己的类型结构,以满足需求。
本文会对c语言中的结构体进行详细介绍,包括:

  • 结构体:
    • 结构体类型的声明
    • 结构的自引用
    • 结构体变量的定义和初始化
    • 结构体内存对齐
    • 结构体传参
    • 结构体实现位段(位段的填充&可移植性)
  • 枚举
    • 枚举类型的定义
    • 枚举的优点
    • 枚举的使用
  • 联合
    • 联合类型的定义
    • 联合的特点
    • 联合的使用

一、结构体

1. 结构的基础知识

结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。
与数组作区分:
虽然数组也是一组值的集合,但数组的这组值的类型相同;

2. 结构的声明

struct Tag
{
	member-list;
} variable-list;
// 使用关键字struct来声明一个结构体
// Tag是结构体的名字
// {}中成员变量
// {}后面的是结构体变量

例如用结构体来描述一个学生

struct Stu
{
	char name[20]; // 名字
	int age; // 年龄
	char sex[5]; // 性别 
	char id[10]; // 学号
}; // 分号不能省略

3. 特殊的声明

除了上面的声明方式外,在结构体的声明时可以不完全声明

// 还可以不声明结构体的名字,定义一个匿名结构体
struct 
{
	member-list;
} variable-list;
// 但是通过这种方法定义的结构体,就只有在定义这里创建的几个结构体变量,之后无法定义一个新的变量,因为没有名字。

struct
{
	int a;
	short b;
	char c;
	double d;
} x;

// 这个结构体只有x这一个变量,无法定义新变量

struct
{
	int a;
	short b;
	char c;
	double d;
} *p;
// 这个结构体只有p这一个指针变量,无法定义新变量

上面的两个结构体都省略了结构体标签Tag,那么问题来了:

// 在上面代码的基础上下面代码合法吗?
p = &x;

非法
因为编译器会将上面的两个声明当作两个完全不同的类型,所以这种行为是非法的。

4. 结构的自引用

结构体中的成员变量类型还可以是其它的结构体:

struct A
{
	int a;
	short b;
	char c;
	float d;
};

struct B
{
	int data;
	struct A a;
};

那么有一个问题,一个结构体的成员变量能否是它自身呢?

struct Node
{
	int data;
	struct Node next;
};
// 这可行吗?
// 如果可行,这个结构体在内存中占多大空间呢?
// 即sizeof(struct Node)是多少

显然这种行为是不可行的,结构体中的成员变量是这个结构体,那么这个结构体在内存中占多大空间就无法确认;

正确的引用方式:

struct Node
{
	int data;
	struct Node* next;
};
// 通过指针来实现结构体的自引用
// 指针的大小是根据机器的平台决定的,这是一个确定的值,所以这个结构体在内存中占据的空间是确定的

注意,当我们使用typedef来重命名结构体时,通常都会使用不完全的结构体声明
如:

typedef struct
{
	int data;
	char ch;
} Test;
// 这是一个匿名结构体,并将其重命名为Test

那么下面代码正确吗?

typedef struct
{
	int data;
	Test* node;
} Test;

不行!
因为,是先有了这个结构体,才能将其重命名为Test,但是在结构体的成员变量中先使用Test,这样就产生了冲突,所以这种方式是不合法的;
应该使用下面的方式进行结构体的自引用;

typedef struct Node
{
	int data;
	struct Node* node;
} Node;

5. 结构体变量的定义和初始化

有了结构体类型,之后应该如何定义变量呢?

#include <stdio.h>

struct Point
{
	int x;
	int y;
} p1; // 声明类型的同时定义一个变量p1

struct Point p2; // 定义变量p2,struct Point是这个类型的名字

// 初始化
struct Point p3 = { x, y }; 
// 这里的x, y表示int类型的数值
// 同数组相同,一组元素使用{}初始化赋值,可以不完全初始化,后续的成员变量会被初始化为0

struct Stu // 声明类型
{
	char name[20]; // 名字
	int age; // 年龄
};
struct Stu stu = { "zhangsan" ,22 }; // 初始化

struct Node
{
	int data; 
	struct Point p;
	struct Node* next;
} n1 = { 10, { 4, 5 }, NULL }; // 结构体嵌套初始化

struct Node n2 = { 20, { 3, 7 }, &n1 }; // 结构体嵌套初始化

6. 结构体内存对齐

通过上面的学习,我们已经掌握了结构体的基本使用了,但是一个结构体在内存中占据多大空间呢?它的成员变量的空间是如何分布的呢?
下面我们就来看看结构体内存对齐

#include <stdio.h>

int main()
{
	struct S1
	{
		char c1;
		int i;
		char c2;
	};
	printf("S1 = %d\n" , sizeof(struct S1));

	struct S2
	{
		char c1;
		char c2;
		int i;
	};
	printf("S2 = %d\n", sizeof(struct S2));

	struct S3
	{
		double d;
		char c;
		int i;
	};
	printf("S3 = %d\n", sizeof(struct S3));

	struct S4
	{
		char c;
		struct S3 s3;
		double d;
	};
	printf("S4 = %d\n", sizeof(struct S4));

	return 0;
}

S1和S2的成员变量类型相同,只是排序不同,它们在内存中占据的空间相同吗?
一个结构体在内存中占据空间的大小,是成员变量占据内存空间的大小之和吗?

运行结果:
在这里插入图片描述
可以看到,S1和S2的大小不同;
结构体占据的内存空间大小也不等于它的成员变量占据空间之和;

那么,结构体变量的内存大小应该如何计算呢?
首先,我们先了解结构体的对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处;

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值

    • VS中默认为8,例如一个int类型的成员变量的对齐数为4,一个char类型的成员变量的对齐数为1,一个空间大于8的结构体类型的对齐数为8;
  3. 结构体的总大小为最大对齐数(每个成员都有一个对齐数)的整数倍,这个结构体的对齐数就是它的最大对齐数;

  4. 如果嵌套了结构体,这个嵌套的结构体对齐到它的最大对齐数的整数倍处,结构体整体大小就是所有最大对齐数(包括嵌套的结构体的对齐数)的整数倍;

那么为什么要在内存对齐呢?(大部分资料)

  1. 平台原因
    不是所有的硬件平台都能够访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出异常;
  2. 性能原因
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐;
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    例如,一个int类型,在32位机器上,一次读取能够获取4个字节,如果这个int类型的数据在内存上不对齐,就不能拿到完整的数据,需要多读取一次;
    在这里插入图片描述
    总的来说,结构体的内存对齐就是拿空间换时间的做法;

那么相同的成员类型,不同的排序顺序,结构体占据的内存空间也可能是不同的,为了节省空间,应该:让占空间小的成员尽量集中在一起

struct S1
{
	char c1;
	int i;
	char c2;
};

struct S2
{
	char c1;
	char c2;
	int i;
};
// s1和S2的成员一模一样,但是它们占据的内存空间大小是不一样的

练习
现在对这部分最开始给出的4个结构体占据的空间进行详细分析:
在这里插入图片描述
结构体的第一个成员的偏移量为0,所以c1在这个结构体的最前面占一个字节,int类型占4个字节,所以一个int类型的成员的对齐数为4,它的起始位置的偏移量为4的整数倍,所以此时需要先空3个字节,i从第四个字节开始存放,char类型占一个字节,对齐数为1,地址地址的偏移量为1的整数倍,所以可以接着继续放,最后因为结构体的总大小为所有成员的最大对齐数的整数倍,此时的最大对齐数为4,所以此时需要再补3个字节,补成12字节,所以一个S1类型的变量占12个字节;
在这里插入图片描述
分析同上,不再赘述;
在这里插入图片描述

在这里插入图片描述
第一个成员c占1个字节,从0偏移处开始,第二个成员是一个结构体,由上面的分析得知,S3占16个字节,所以它的对齐数为8,此时需要填充7个字节,从第8字节开始,最后是一个对齐数为8的double类型,开始位置的偏移量应为8的倍数,所以S4这个类型占32个字节的空间;

7. 修改默认对齐数

使用#pragma这个预处理指令可以修改默认对齐数;

#include <stdio.h>

#pragma pack(8) // 设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
};

#pragma pack() // 取消设置的默认对齐数,还原为默认的8 -- VS中

#pragma pack(1) // 将默认对齐数设为1
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack() // 还原默认对齐数

int main()
{
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));

	return 0;
}

分析:
在这里插入图片描述
在这里插入图片描述

运行结果:
在这里插入图片描述

结构体在对齐方式不合适的时候,可以自己更改默认对齐数;

百度面试题:
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明,参考offsetof宏的实现

#define OFFSETOF(type, member) (unsigned int)(&(((type*)0)->member))
// 将0作为一个结构体类型的指针,通过指针->访问到对应成员member,再取地址,
// 因为这个结构体类型的地址为0,所以此时这个成员的地址就相当于它的偏移量,再将这个地址转为无符号整数,
// 这样就能获取到一个结构体类型中某成员变量相对于首地址的偏移

#include <stdio.h>

struct Stu
{
	char name[20];
	int age;	
	char sex[5];
	char id[10];
};

int main()
{
	printf("%d\n", OFFSETOF(struct Stu, age));

	return 0;
}

运行结果:
在这里插入图片描述

注意,这里的数组的对齐数是这个数组中元素的对齐数,它是一个char类型的数组,所以这个数组的对齐数为1
这与嵌套结构体的对齐数相同,结构体的对齐数为最大对齐数,是这个结构中的成员变量的最大的对齐数,数组中元素相同,所以最大对齐数就是数组元素的对齐数;

8. 结构体传参

直接看代码,下面的代码1和代码2哪个更好

#include <stdio.h>

struct S
{
	int data[1000];
	int num;
};

struct S s = { { 1,2,3,4 }, 1000 };

// 代码1
void print1(struct S s)
{
	printf("%d\n", s.num);
}

// 代码2
void print2(struct S* s)
{
	printf("%d\n", s->num);
}

int main()
{
	print1(s); // 传结构体
	print2(&s); // 传指针

	return 0;
}

代码1是传值调用,代码2是传址调用;
我们都知道,
传值调用,是将实参拷贝一份,函数对临时变量进行处理;
传址调用,是将实参的地址传给函数,函数能通过指针,直接对参数进行处理;
传值相比传址会多做一次拷贝,存在额外开销,当结构体类型占据空间很庞大时,这个额外开销也就随着增大;
所以从性能角度出发,代码2的传址调用更好;

所以在结构体传参时,要传结构体的地址

二、位段

1. 什么是位段

位段的声明和结构体是类似的,只有两个不同:

  1. 位段的成员必须是intunsigned intsigned intcharunsigned charsigned char
  2. 位段的成员名后面有一个冒号:和一个数字

比如:

struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};

A就是一个位段类型,那么位段A占多大的内存呢?
8个字节;
怎么来的呢?
每个成员的:后面的数字表示这个成员占多少个比特位;
int表示每次分配空间都分配一个int类型的大小,也就是4个字节,然后使用这4个字节为成员分配空间,直到剩余空间不足,继续分配一个int类型的大小;
在这个例子中,先位段分配4个字节,成员_a占2个bit,此时还剩30个bit;成员_b占5个bit,此时还剩25个bit;成员_c占10个bit,此时还剩15个bit;成员_d需要30个bit,剩余的空间不足,所以再分配4个字节,此时要怎么使用呢?
是使用了剩余的15bit,再从新的32bit中分配15bit;还是直接从新的32bit中分配30bit?
正确的是后者。
所以这个位段占的空间大小为8个字节;
在这里插入图片描述

2. 位段的内存分配

  1. 位段的成员可以是intunsigned intsigned int或者是char类型(属于整形家族);
  2. 位段的空间是按照需要以4个字节(int类型)或者1个字节(char类型)的方式来开辟的;
  3. 位段涉及很多不确定因素,所以位段是不跨平台的,注重可移植的程序应该避免使用位段;
struct S
{
	char a:3;
	char b:4;
	char c:5;
	char d:4;
};

int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;

	return 0;
}

在这里插入图片描述

注意:
位段的成员的长度是有限制的,
char类型的成员长度不能超过8;
int类型的成员长度不能超过32;(32位或64位机器下)

3. 位段的跨平台问题

  1. int位段被当成有符号数还是无符号数是不确定的;
  2. 位段中最大位的数目是不确定。(16位机器下,最大为16,写成27,在16位机器中是会出问题的);
  3. 位段中的成员在内存中从左向右,还是从右向左分配标准尚未定义;
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的;

总结
与结构相比,位段可以达到同样的效果,但是可以更好的节省空间,但是会存在跨平台问题;

4. 位段的应用

像计算机网络中的IP数据报的格式,就是使用了位段。这样节省了传递的报文的长度,使得报文的有效数据更高。具体就不展开介绍了。

注意
因为位段的成员的开始地址并不是一个字节的起始位置,只有字节是有地址的,比特位是没有地址的,所以&s._a这种行为是错误的,要输入值到一个位段的成员,是不能直接使用scanf函数的,只能将值输入到一个变量中,通过赋值,将这个值传给位段成员;

三、枚举

枚举顾名思义就是一一列举。
比如,一周有7天,从星期一到星期天是有限的7天,可以一一列举;
月份有12个月,也可以一一列举;
这里就能使用枚举了。

1. 枚举类型的定义

enum Day // 星期
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

enum Color // 颜色
{
	Red,
	Green,
	Blue
};

enum Month // 月份
{
	Jan,
	Feb,
	Mar,
	Apr,
	May,
	Jun,
	Jul,
	Aug,
	Sep,
	Oct,
	Nov,
	Dec
};

以上定义的enum Dayenum Colorenum Month都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫做枚举常量

这些可能的取值是可确定的,默认从0开始,依次递增1,在定义的时候也可以赋初值。
如:

enum Color // 颜色
{
	Red, // 0
	Green, // 1
	Blue // 2
};

enum Color // 颜色
{
	Red = 1, // 1
	Green = 3, // 3
	Blue // 4
};

枚举是一种类型(枚举类型),它的成员是常量(枚举常量),枚举常量的类型是int;

2. 枚举的优点

为什么要用枚举呢?

  1. 增加代码的可读性和可维护性;
  2. #define定义的标识符比较,枚举有类型检查,更加严谨;
  3. 防止命名污染(封装);
  4. 便于调试;
  5. 使用方便,可一次定义多个常量;

例如:前面我们写过的计算器

void menu()
{
	printf("*******************\n");
	printf("*** 1.Add 2.Sub ***\n");
	printf("*** 3.Mul 4.Div ***\n");
	printf("*** 0.exit      ***\n");
	printf("*******************\n");
}

enum Op
{
	exit,
	add,
	sub,
	mul,
	div
};

int main()
{
	int input;
	printf("请选择:>");
	scanf("%d", &input);
	// 代码1
	switch (input)
	{
	case 0:
		// 退出
		break;
	case 1:
		Add();
		break;
	case 2:
		Sub();
		break;
	case 3:
		Mul();
		break;
	case 4:
		Div();
		break;
	default:
		// 选错
		break;
	}

	// 代码2
	switch (input)
	{
	case exit:
		// 退出
		break;
	case add:
		Add();
		break;
	case sub:
		Sub();
		break;
	case mul:
		Mul();
		break;
	case div:
		Div();
		break;
	default:
		// 选错
		break;
	}

	return 0;
}

这段代码中,代码1如果不看menu函数,估计没人知道case 1这些分支表示什么意思,但是代码2中使用了枚举类型,就能一眼看出每个分支是干什么的,增加了代码的可读性;

#define的功能是直接替换,并不会因为类型不同而产生不同的操作;
除此之外,#define定义的标识符常量是全局的,而枚举类型的值只能被该类型的变量访问;

从源程序到可执行程序再到执行经过的过程为:
在这里插入图片描述
#define的替换是在预编译过程中完成的,调试无法发现#define带来的错误;

3. 枚举的使用

使用上面的Color类型:

int main()
{
	enum Color color1 = Blue; 
	enum Color color2 = 2; // err

	return 0;
}

直接将int类型赋值给枚举类型是错误的,虽然Blue这个枚举常量的值也是2,但是它的类型的enum Color这个枚举类型,c语言是会对枚举类型进行类型检查的;

四、联合

1. 联合类型的定义

联合也是一种特殊的自定义类型;
这种类型定义的变量也包含一系列的成员,特征是这些成员都共用一块内存空间(所以,联合也叫共用体);

比如:

#include <stdio.h>

union Un
{
	char ch;
	int i;
} ;

union Un1
{
	double d;
	char ch;
	int i;
};

int main()
{
	printf("%d\n", sizeof(union Un));
	printf("%d\n", sizeof(union Un1));

	return 0;
}

运行结果:
在这里插入图片描述

2. 联合的特点

联合的成员共用一块内存空间,这样一个联合变量的大小,至少也是最大成员的大小,因为联合至少要能保存它最大成员的值;

下面代码输出什么?

#include <stdio.h>

union Un
{
	int i;
	char c;
};

int main()
{
	union Un un;
	printf("%p\n", &un);
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));

	return 0;
}

分析:
因为联合体变量un有两个成员i、c,这个联合变量占4个字节,通过内存地址的使用都是从低位到高位,所以i占整4个字节,c占地址最小的1个字节;

运行结果:
在这里插入图片描述
在这里插入图片描述

下面代码的输出又是什么呢?

int main()
{
	union Un un;
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);

	return 0;
}

分析:
在这里插入图片描述
运行结果:
在这里插入图片描述

面试题
判断当前计算机的大小端存储

之前实现方式是将一个int类型的变量赋值为1,
如果机器是大端字节序,那么它在内存中的存储为(低到高):00 00 00 01;
如果机器是小端字节序,那么它在内存中的存储为(低到高):01 00 00 00;
取这个int类型变量的地址,将其转换为char*类型,访问这个指针指向的内存空间的内容(处于低位的第一字节):
如果为0,则为大端;如果为1,则为小端;
这种方法和联合的使用刚好相符,所以此时可以使用联合类型,就不需要强制类型转换了;
具体代码如下:

#include <stdio.h>

union Un
{
	char c;
	int i;
};

int main()
{
	union Un un;
	un.i = 1;
	if (un.c)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}

	return 0;
}

3. 联合大小的计算

  • 联合的大小至少是最大成员的大小;
  • 当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍;

举个例子:

#include <stdio.h>

union Un1
{
	char c[5];
	int i;
};

union Un2
{
	short c[7];
	int i;
};

int main()
{
	// 下面代码输出什么?
	printf("%d\n", sizeof(union Un1));
	printf("%d\n", sizeof(union Un2));

	return 0;
}

分析:
根据上面的规则,
Un1中最大对齐数是i的对齐数4,最大成员的大小是c数组的大小5,此时5不是4的整数倍,所以此时的联合的大小为8;
Un1中最大对齐数是i的对齐数4,最大成员的大小是c数组的大小14,此时14不是4的整数倍,所以此时的联合的大小为16;

运行结果:
在这里插入图片描述

4. 联合休的使用实例:

使用联合体是可以节省空间的,例如:
现在要举办一个活动,要上线一个礼品兑换单,礼品兑换单有三种商品:图书、杯子、衬衫。
每种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。

图书:书名、作者、页数;
杯子:设计;
衬衫:设计、可选颜色、可选尺寸;

如果不认真思考,可能就会直接使用下面类似的结构:

struct gift_list
{
	// 公共属性
	int stock_number; // 库存量
	double price; // 价格
	int item_type; // 商品类型

	// 特殊属性
	// 图书
	char title[20]; // 书名
	char author[20]; // 作者
	int num_pages; // 页数

	// 杯子
	char design[30]; // 设计

	// 衬衫
	// 与杯子共用设计成员属性
	int colors; // 颜色
	int sizes; // 尺寸
};

这样的结构设计非常简单,但结构中包含了所有礼品的属性,对于一个礼品来说有些属性是不需要的,使用这个结构记录数据是会浪费内存的;
上面的礼品中只有一些属性是共用,对于那些特殊属性就可以使用联合体来保存,以此来节省一些内存;
所以上面的代码可以改成:

struct gift_list
{
	// 公共属性
	int stock_number; // 库存量
	double price; // 价格
	int item_type; // 商品类型

	// 使用匿名联合体,在声明时定义一个变量item
	union
	{
		// 这个联合体中的成员是三个匿名结构体的变量,book、cup、shirt
		struct
		{
			// 图书
			char title[20]; // 书名
			char author[20]; // 作者
			int num_pages; // 页数
		} book;
		struct
		{
			// 杯子
			char design[30]; // 设计
		} cup;
		struct
		{
			// 衬衫
			char design[30]; // 设计
			int colors; // 颜色
			int sizes; // 尺寸
		} shirt;
	} item;
	// 特殊属性
};

五、使用结构体实现一个通讯录

contact.c

#define _CRT_SECURE_NO_WARNINGS

#include "contact.h"

void initContact(Contact* contact)
{
	assert(contact);

	memset(contact->data, 0, MAXNUM * sizeof(Peoinfo));
	contact->count = 0;
}

static int isFull(const Contact* contact)
{
	if (contact->count == MAXNUM)
	{
		return 1; // 通讯录已满
	}
	return 0; // 通讯录未满
}

void Add(Contact* contact)
{
	assert(contact);
	if (isFull(contact))
	{
		printf("通讯录已满,无法再增加信息\n");
		return;
	}

	getchar();
	// 没有做溢出检查
	printf("输入名字:>");
	gets((contact->data[contact->count]).name); 

	printf("输入年龄:>");
	scanf("%d", &((contact->data[contact->count]).age));

	getchar();
	printf("输入性别:>");
	gets((contact->data[contact->count]).sex);

	//getchar();
	printf("输入电话:>");
	gets((contact->data[contact->count]).phone_number);

	//getchar();
	printf("输入地址:>");
	gets((contact->data[contact->count]).address);

	(contact->count)++;
}

int isEmpty(const Contact* contact)
{
	if (contact->count == 0)
	{
		return 1; // 通讯录为空
	}
	return 0;
}

void Print(const Contact* contact)
{
	assert(contact);
	if (isEmpty(contact))
	{
		printf("通讯录为空\n");
		return;
	}

	printf("%-20s%-7s%-7s%-20s%-30s\n", "名字", "年龄", "性别", "电话", "地址");
	int i = 0;
	for (i = 0; i < contact->count; i++)
	{
		printf("%-20s%-7d%-7s%-20s%-30s\n", (contact->data[i]).name,
				(contact->data[i]).age,
				(contact->data[i]).sex,
				(contact->data[i]).phone_number,
				(contact->data[i]).address);
	}
}

static int search_by_name(const Contact* contact)
{
	assert(contact);
	if (isEmpty(contact))
	{
		printf("通讯录为空\n");
		return;
	}

	char buffer[20] = { 0 };
	printf("输入要查找人的名字:>");
	getchar();
	gets(buffer);
	int i = 0;
	for (i = 0; i < contact->count; i++)
	{
		// 找到了,只能找到第一个相符的人
		if (strcmp((contact->data[i]).name, buffer) == 0)
		{
			return i;
		}
	}
	return -1; // 未找到
}


void Delete(Contact* contact)
{
	// 找到指定人
	// 通过名字查找
	int ret = search_by_name(contact);
	if (ret == -1)
	{
		printf("没有这个人\n");
		return;
	}
	
	(contact->count)--;
	int i = 0;
	for (i = ret; i < contact->count; i++)
	{
		strcpy((contact->data[i]).name, (contact->data[i + 1]).name);
		(contact->data[i]).age = (contact->data[i + 1]).age;
		strcpy((contact->data[i]).sex, (contact->data[i + 1]).sex);
		strcpy((contact->data[i]).phone_number, (contact->data[i + 1]).phone_number);
		strcpy((contact->data[i]).address, (contact->data[i + 1]).address);
	}
}

void Modify(Contact* contact)
{
	assert(contact);
	// 找到指定人
	// 通过名字查找
	int ret = search_by_name(contact);
	if (ret == -1)
	{
		printf("没有这个人\n");
		return;
	}

	printf("进行修改\n");
	// 没有做溢出检查
	printf("输入名字:>");
	gets((contact->data[ret]).name);

	printf("输入年龄:>");
	scanf("%d", &((contact->data[ret]).age));

	getchar();
	printf("输入性别:>");
	gets((contact->data[ret]).sex);

	//getchar();
	printf("输入电话:>");
	gets((contact->data[ret]).phone_number);

	//getchar();
	printf("输入地址:>");
	gets((contact->data[ret]).address);
}

void Search(const Contact* contact)
{
	assert(contact);
	int ret = search_by_name(contact);
	if (ret == -1)
	{
		printf("没有这个人\n");
		return;
	}

	printf("%-20s%-7s%-7s%-20s%-30s\n", "名字", "年龄", "性别", "电话", "地址");

	printf("%-20s%-7d%-7s%-20s%-30s\n", (contact->data[ret]).name,
		(contact->data[ret]).age,
		(contact->data[ret]).sex,
		(contact->data[ret]).phone_number,
		(contact->data[ret]).address);
}

void Sort(Contact* contact)
{
	assert(contact);

	// 使用冒泡排序
	int i = 0;
	int j = 0;
	for (i = 0; i < (contact->count) - 1; i++)
	{
		for (j = 0; j < (contact->count) - i - 1; j++)
		{
			if (strcmp((contact->data[j]).name, (contact->data[j + 1]).name) > 0)
			{
				char buffer[30] = { 0 };
				// 交换名字
				strcpy(buffer, (contact->data[j]).name);
				strcpy((contact->data[j]).name, (contact->data[j + 1]).name);
				strcpy((contact->data[j + 1]).name, buffer);
				// 交换年龄
				int tmp = (contact->data[j]).age;
				(contact->data[j]).age = (contact->data[j + 1]).age;
				(contact->data[j + 1]).age = tmp;
				// 交换性别
				strcpy(buffer, (contact->data[j]).sex);
				strcpy((contact->data[j]).sex, (contact->data[j + 1]).sex);
				strcpy((contact->data[j + 1]).sex, buffer);
				// 交换电话
				strcpy(buffer, (contact->data[j]).phone_number);
				strcpy((contact->data[j]).phone_number, (contact->data[j + 1]).phone_number);
				strcpy((contact->data[j + 1]).phone_number, buffer);
				// 交换地址
				strcpy(buffer, (contact->data[j]).address);
				strcpy((contact->data[j]).address, (contact->data[j + 1]).address);
				strcpy((contact->data[j + 1]).address, buffer);
			}
		}
	}
}

contact.h

#pragma once

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

// 定义结构体、标识符常量
#define MAXNUM 1000 // 通讯录中能保存的人数

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

typedef struct
{
	Peoinfo data[MAXNUM]; // 通讯录可以保存1000个人的信息
	int count; // 当前通讯录中的人数
} Contact;


// 声明函数
// 初始化通讯录
void initContact(Contact*);
// 增加人的信息
void Add(Contact*);
// 打印通讯录
void Print(Contact*);
// 删除指定人的信息
void Delete(Contact*);
// 修改指定人的信息
void Modify(Contact*);
// 查找指定人的信息
void Search(Contact*);
// 排序通讯录
void Sort(Contact*);

test.c

#include "contact.h"

void menu()
{
	printf("*******************************\n");
	printf("***  1.增加信息 2.删除信息  ***\n");
	printf("***  3.修改信息 4.查找信息  ***\n");
	printf("***  5.排序信息 6.打印信息  ***\n");
	printf("***  0.退出程序             ***\n");
	printf("*******************************\n");
}

enum Op
{
	exit, // 退出程序
	add, // 增加信息
	delete, // 删除信息
	modify, // 修改信息
	search, // 查找信息
	sort, // 排序信息
	print // 打印信息
};

int main()
{
	int input;
	// 通讯录
	Contact contact;
	// 初始化通讯录
	initContact(&contact);

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case exit:
			printf("退出通讯录\n");
			break;
		case add:
			Add(&contact);
			break;
		case delete:
			 Delete(&contact);
			break;
		case modify:
			 Modify(&contact);
			break;
		case search:
			 Search(&contact);
			break;
		case sort:
			 Sort(&contact);
			break;
		case print:
			 Print(&contact);
			break;
		default:
			printf("没有这个选项\n");
			break;
		}

	}while(input);

	return 0;
}

其中contact.c中的排序函数可以使用回调函数来实现让用户提供排序方法,不一定要用名字作排序;


总结

本文对c语言中会用到的结构体类型进行了详细解读,并在最后给出了一个使用结构体实现的通讯录;


文章转载自:
http://antebrachium.wsgyq.cn
http://amorce.wsgyq.cn
http://bha.wsgyq.cn
http://blazer.wsgyq.cn
http://alms.wsgyq.cn
http://burier.wsgyq.cn
http://bitterness.wsgyq.cn
http://baoding.wsgyq.cn
http://aboulia.wsgyq.cn
http://chanceless.wsgyq.cn
http://bolograph.wsgyq.cn
http://cheering.wsgyq.cn
http://antichristianism.wsgyq.cn
http://bursarial.wsgyq.cn
http://argument.wsgyq.cn
http://bryology.wsgyq.cn
http://boulogne.wsgyq.cn
http://aptotic.wsgyq.cn
http://aeromagnetics.wsgyq.cn
http://barrater.wsgyq.cn
http://antidiabetic.wsgyq.cn
http://bludger.wsgyq.cn
http://carbine.wsgyq.cn
http://amerce.wsgyq.cn
http://apiculus.wsgyq.cn
http://breadbox.wsgyq.cn
http://axiomatic.wsgyq.cn
http://athetoid.wsgyq.cn
http://anastatic.wsgyq.cn
http://backbiting.wsgyq.cn
http://www.dtcms.com/a/111376.html

相关文章:

  • Windows 11 听的见人声,但是听不见背景音乐或者听不见轻音乐等,可以这样设置
  • 【橘子大模型】Runnable和Chain以及串行和并行
  • STM32 HAL库 CANFD配置工具
  • 小程序API —— 58 自定义组件 - 创建 - 注册 - 使用组件
  • CExercise_04_1运算符_6 (扩展) 找出数组中只出现一次的唯二元素
  • 社会视频汇聚:构筑城市安全防线的智慧之眼
  • VirtualBox 配置双网卡(NAT + 桥接)详细步骤
  • 《微服务概念进阶》精简版
  • 新浪财经股票每天10点自动爬取
  • 免费送源码:Java+SSM+Android Studio 基于Android Studio游戏搜索app的设计与实现 计算机毕业设计原创定制
  • Springboot + Vue + WebSocket + Notification实现消息推送功能
  • 接口自动化学习四:全量字段校验
  • L1-100 四项全能(测试点1)
  • 计算机网络知识点汇总与复习——(三)数据链路层
  • 在VMware下Hadoop分布式集群环境的配置--基于Yarn模式的一个Master节点、两个Slaver(Worker)节点的配置
  • Leetcode 33 -- 二分查找 | 归约思想
  • 【YOLO系列(V5-V12)通用数据集-交通红黄绿灯检测数据集】
  • SpringBoot集成swagger和jwt
  • Flask学习笔记 - 模板渲染
  • 深入探究 Hive 中的 MAP 类型:特点、创建与应用
  • 【Linux系统编程】进程概念,进程状态
  • 第三期:深入理解 Spring Web MVC [特殊字符](数据传参+ 特殊字符处理 + 编码问题解析)
  • 游戏编程模式学习(编程质量提升之路)
  • 25.4.4错题分析
  • Linux: network: 两台直连的主机业务不通
  • 【移动编程技术】作业1 中国现代信息科技发展史、Android 系统概述与程序结构 作业解析
  • Leetcode——150. 逆波兰表达式求值
  • 【小沐杂货铺】基于Three.JS绘制三维数字地球Earth(GIS 、three.js、WebGL、vue、react)
  • 平台总线---深入分析
  • transforms-pytorch4