【C语言】自定义类型(构造类型)——结构体、枚举和联合体


🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🐌 个人主页:蜗牛牛啊
🔥 系列专栏:🛹初出茅庐C语言、🛴数据结构
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹


一、结构体

结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。

1.结构体的声明

struct tag {
	member - list;
}variable - list;

struct是结构体关键字,tag是结构体标签,member - list;是成员变量列表,variable-list是变量列表。结构体的成员可以标量、数组、指针甚至是其他结构体。
例如要用结构描述一个人的基本信息时,我们可以用结构体声明,如下:

struct popInfo {
	char name[20];//姓名
	char sex[2];//性别
	int age;//年龄
	char id[20];//身份证号
} s1;

其中popInfo是结构体标签,{ char name[20]; char sex[2]; int age; int id[20];是结构体成员,要注意最后的;不能省略s1是结构体变量。
但是我们在平时阅读代码的时候可能会看到这种代码:

//匿名结构体类型
struct {
	char name[20];//姓名
	int age;//年龄
	char id[20];//学号
};

这种声明省略了结构体的标签,是一种不完全的声明(匿名结构体类型),要想创建变量必须在结构体声明的时候直接定义并初始化。
下面这段代码只在结构体声明时定义了两个结构体全局变量s1s2,并没有对其初始化,在VS2022下会提示不正确。

struct {
	char name[20];//姓名
	int age;//年龄
	char id[20];//学号
}s1, s2;
int main()
{
	//struct s3;//这样定义结构体变量是不可行的
	s1 = { "zhangsan" ,20, "123456789" };//err,在VS2022下报错
	s2 = { "lisi" ,19, "147258369" };//err,在VS2022下报错
	return 0;
}

在对匿名结构体类型进行定义时,还要对其进行初始化。例如下面这段代码:

#include <stdio.h>
struct {
	char name[20];//姓名
	int age;//年龄
	char id[20];//学号
}s1 = { "zhangsan" ,20, "123456789" }, s2 = { "lisi" ,19, "147258369" };
int main()
{
	//struct s3;//这样定义结构体变量是不可行的
	//s1 = { "zhangsan" ,20, "123456789" };//err,会报错
	//s2 = { "lisi" ,19, "147258369" };//err,会报错
	printf("%s %d %s\n", s1.name, s1.age, s1.id);//输出结果为zhangsan 20 123456789
	printf("%s %d %s\n", s2.name, s2.age, s2.id);//输出结果为lisi 19 147258369
	return 0;
}

那么两个相同的匿名结构体,它们在声明时所定义的变量类型时相同的吗?答案是不相同。代码如下:

struct {
	char name[20];//姓名
	int age;//年龄
	char id[20];//学号
}s;
struct {
	char name[20];//姓名
	int age;//年龄
	char id[20];//学号
}*p;
int main()
{
	p = &s;
	return 0;
}

运行代码后,会有如下警告:

p=&s
编译器会把上面的两个声明当成完全不同的类型,所以是非法的。

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

在定义结构体变量的同时给结构体变量赋值叫做结构体变量的初始化。

#include <stdio.h>
struct popInfo {
	char name[20];//姓名
	char sex[5];//性别
	int age;//年龄
	char id[20];//身份证号
} s1;//声明类型的同时定义变量s1(定义时没有在函数内时s1是全局变量)。
struct popInfo p1;//定义结构体变量p1,p1是全局变量
int main()
{
	struct popInfo s2;//一般在main函数中定义,s2是局部变量
	struct popInfo s3 = { "zhangsan","nan",20,"123546879" };//初始化,s3是局部变量
	printf("%s %s %d %s\n", s3.name, s3.sex, s3.age, s3.id);//输出结果为zhangsan nan 20 123546879
	s3.age = 30;//修改结构体变量s3中的变量值
	printf("%s %s %d %s\n", s3.name, s3.sex, s3.age, s3.id);//输出结果为zhangsan nan 30 123546879
	//s3.name = "lisi";//err,s3.name拿到的是name数组中首元素的地址,给地址赋值不可行
	//*(s3.name) = "lisi";//得不到想要的效果,*(s3.name)得到的是数组name中的首字符,给首字符赋值是不正确的
	//scanf("%s", s3.name);//改变name数组中的值
	strcpy(s3.name, "lisi");//将字符串"lisi"拷贝给name数组,改变了name数组中的值
	printf("%s %s %d %s\n", s3.name, s3.sex, s3.age, s3.id);
	return 0;
}

3.结构体的嵌套

结构体里面包含另一个结构体,叫做结构体的嵌套。

#include <stdio.h>
struct S {
	int x;
	int y;
};
struct P {
	int x;
	struct S s1;
	char ch;
};
int main()
{
	struct P p1 = { 5,{10,15},'z' };
	printf("%d %d %d %c\n", p1.x, p1.s1.x, p1.s1.y, p1.ch);//输出结果为 5 10 15 z
	//嵌套的话先找结构体变量再找结构体成员
	return 0;
}

4.结构体的自引用

结构体的自引用:在结构中包含一个类型为该结构本身的成员。
如果用下面的代码声明结构体不能得出结构体的大小,sizeof(struct Node)无法计算。

struct Node {
	int data;
	struct Node next;
};

那我们可以利用一个struct Node类型的指针记录下一个结构体的地址,这样便能实现结构体的自引用。一个struct Node指针的大小也就是4/8个字节,通过这样的方式能够计算出结构体的大小。struct Node* nextnext是指针,指向的类型是struct Node
我们还可以利用typedef关键字对结构体进行类型重命名,Node此时就是struct Node类型。结构体声明中不能直接利用Node* next

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

结构体的自引用

5.结构体内存对齐

在学习结构体内存对齐之前我们先学习一个函数 offsetof,它的头文件是stddef.h
offsetof
offset的两个参数,type是结构体类型,member是结构体成员名,offset作用是计算结构体成员相对于起始位置的偏移量。

#include <stdio.h>
#include <stddef.h>
struct S {
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct S, c1));//0
	printf("%d\n", offsetof(struct S, i));//4
	printf("%d\n", offsetof(struct S, c2));//8
	printf("%d\n", sizeof(struct S));//12
	return 0;
}

通过上面的代码我们可以知道struct Sc1相对于起始位置偏移量是0,i相对于起始位置偏移量是4,c2相对于起始位置的偏移量是8。通过下面一张图加深理解:空间分配
通过上面的代码我们知道结构体的大小是12个字节,但是图中总共利用的空间也就只有9个字节,但是为什么结构体的大小是12个字节呢?这里我们就要引入结构体的内存对齐规则。
结构体的内存对齐规则:
(1)结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处。
(2)从第二个成员开始,要对齐到某个对齐数的整数倍的偏移处。(对齐数:结构体成员自身大小和默认对齐数中的较小值,VS下默认对齐数是8个字节;Linux环境下默认不设对齐数,对齐数是结构体成员的自身大小)
(3)结构体的总大小必须是最大对齐数的整数倍。
(4)如果嵌套了结构体的情况,嵌套的结构体对齐到最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)中最大对齐数的整数倍。

那么通过结构体内存对齐规则,我们分析一下下面的代码:

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

struct S中,c1本身是char类型的,占1个字节,VS环境下默认对齐数是8,根据对齐数的定义可以知道c1的对齐数是1和8中的最小值,那么c1的对齐数就是1;ichar类型的,它自身大小是4,VS环境下默认对齐数是8,那么i的对齐数就是4;c2char类型的,它自身大小是1,VS环境下默认对齐数是8,那么c2的对齐数就是1。知道了他们的对齐数,那么他们在内存中是怎样存放的呢?我们就来利用结构体的内存对齐规则来解释一下struct S中的成员变量是如何存放的。
struct S
根据结由结构体内存对齐规则我们知道结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处,所以c1存放在偏移量为0的大小为1个字节的空间,结构体中从第二个成员开始,要对齐到某个对齐数的整数倍的偏移处,第二个成员是i,他的对齐数是4,所以要从4的整数倍的偏移处那里开始存放,所以存放在偏移量为4的那个位置,c2的对齐数是1,存放在i的后面,结构体的总大小必须是最大对齐数的整数倍,在struct S中结构体成员的最大对齐数是4,所以结构体的大小是4的整数倍,结构体的大小就是12。
那我们再来计算一下下面结构体的大小。

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

c1的对齐数是1,c2的对齐数是1,i的对齐数是4,那在空间中分配内存时应该是这样的:
分配空间通过上面内存分配图我们可以发现struct Sstruct S1两种类型的结构体成员类型相同,但是结构体所需空间却有很大差异,那么我们可以发现在设计结构体的时候,让占用空间小的成员尽量集中在一起,既能满足空间对齐,又能节省空间。

我们通过下面的代码分析一下在嵌套结构体中内存对齐规则的应用:

struct S2 {
	double d;
	char ch;
	int i;
};
struct S3 {
	char c;
	struct S2 s2;
	double d;
};

分析:struct S2 d的自身大小是8个字节,VS环境下默认对齐数是8,ch的对齐数是1,i的对齐数是4,在struct S3c的对齐数是1,struct S2 是嵌套的结构体,嵌套的结构体对齐到最大对齐数的整数倍处,struct S2 s2中的最大对齐数是8,所以在分配空间时要对齐到8的整数倍处,struct S3d的对齐数是8。
在内存中的空间分配如下:

占用空间图

扩展:结构体在对齐方式不合适的时候,我们可以修改默认对齐数。
设置默认对齐数:pragma pack(n),n是你想要修改的数值,只能是整数。

#pragma pack(n)//设置默认对齐数

恢复默认对齐数:pragma pack()

#pragma pack()//恢复默认对齐数

为什么会存在内存对齐?可以归结为两点:
(1)平台原因(移植原因):不是所有的硬件平台都能访问地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐到内存访问只需要一次访问。
总的来说,结构体的内存对齐就是拿空间换时间的做法。

二、结构体成员的访问和结构体传参

结构体传参可以分为传值调用和传址调用,传值调用的时候函数的参数可以用结构体类型的变量来接收;传址调用的时候函数的参数可以用结构体指针变量来接收。
结构体成员的访问可以有两种方式:结构体变量.结构体成员名;结构体指针->结构体成员名。
我们通过下面的实例来演示:

//打印结构体中的数据
#include <stdio.h>
struct S
{
	int data[10];
	char buf[10];
};
void Print1(struct S s)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d  ", s.data[i]);
	}
	printf("\n");
	printf("%s\n", s.buf);
}
void Print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		//printf("%d  ", (*ps).data[i]);
		printf("%d  ", ps->data[i]);
	}
	printf("\n");
	//printf("%s\n", (*ps).buf);
	printf("%s\n", ps->buf);
}
int main()
{
	struct S s1 = { {666,888,999},"abc" };
	Print1(s1);//传值调用,将s1中的值传给s
	Print2(&s1);//传址调用,将s1的地址传给ps
	return 0;
}

当结构体传参的时候我们首选传址调用,因为传值调用参数压栈系统开销大,会造成系统性能下降。

三、位段

1.位段的声明

位段的声明和结构体是类似的,有两个不同:
(1)位段的成员必须是intunsigned intsigned intchar也可以)
(2)位段的成员名后边有一个冒号和一个数字。后面的数字其实所占的二进制位数。
通过代码来说明位段的声明:

#include <stdio.h>
struct A {
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d", sizeof(struct A));//输出结果为8
	return 0;
}

A就是一个位段类型—sizeof(struct A)大小是8个字节。
位段其中的位其实是二进制位,上面代码的意思是:_a占了2个比特(bit)位,_b占了5个比特(bit)位,_c占了10个比特(bit)位,_d占了30个比特(bit)位。

2.位段的内存分配

(1)位段的成员必须是intunsigned intsigned intcharchar是整型家族,char和int一般不会混着用)。
(2)位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
(3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
现在我们来分析这段代码:

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

刚开始开辟4个字节的空间(4Byte=32bit),给_a分配2个比特位,还剩30bit,再给_b分配5个比特位bit,还剩25个比特位,给_c分配10个比特位之后,还剩15个,不能达到_d所需要的空间,再开辟4个字节的空间,然后给_d30个比特位。所以此时struct A占用了8个字节的空间。
我们通过练习的方式加深对位段内存分配的理解,看下面代码:

#include <stdio.h>
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类型的,刚开始开辟1个字节,给s.a分配3个比特位,但是s.a用二进制表示为1010,位段的存放形式是不确定的,假设是从低位到高位存储的,那s.a放进去的是010;s.b用二进制表示为1100,那放进去的就是1100;s.c用二进制表示为11,刚才的那个字节只剩下1个比特位,需要再开辟1个字节的空间,放进去之后不够5个比特位,高位补0,放进去就是00011;s.d用二进制表示为100,需要4个比特位,刚才开辟只剩下3个比特位,还需要再开辟1个字节用来存放s.d,100不够4个比特位,高位补0,那就是0100。分配的内存可见下图:

位段
在VS2022(X86)环境下可以看到内存中的数据:
内存

扩展:位段的跨平台问题:
(1)int被当成有符号数还是无符号数是不确定的。
(2)位段中最大位数的的数目不能确定(16位机器最大16,32位机器最大32,如果写成27,在16位机器上会出问题。)
(3)位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。
(4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台问题的存在。

四、枚举

枚举顾名思义就是把可能的取值都列举出来。

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

上面的代码中,enum枚举关键字,Day是枚举标签,{ }中是枚举可能的取值。
通过下面的代码去理解枚举:

#include <stdio.h>
int main()
{
	enum Color {
		//枚举里面的每一项都是可能取值,每一个可能的取值都是常量
		red,//刚开始的时候是常量0,每次增长1
		black,
		green=5,//可以在定义的时候更改常量值,后面不能修改
		blue
	};
	printf("%d %d %d\n", red, green, blue);
	enum Color c = red;
	//red = 5;//err,不能在这里修改enum里的常量值
	printf("%d\n", c);
	return 0;
}

解析:输出结果为 0 5 6 0。因为枚举中的第一个可能取值默认是0,可以在设定枚举的时候改变枚举的常量值,red的值是0,black的值是1,因为有green=5,就将green的值改为5,如果不改他的值是2,green的值改变,他后面的常量值也会改变,blue的值为6。enum Color c = red;,将red的值赋给c,所以c的值是0。

枚举类型的大小是4个字节,因为里面的值都是可能取值。
我们可以使用#define定义常量,为什么要使用枚举?因为枚举类型有如下优点:
(1)增加代码的可读性和维护性
(2)和#define定义的标识符比较枚举有类型检查,更加严谨。
(3)防止了命名污染(封装)
(4)便于调试在这里插入代码片
(5)使用方便,一次可以定义多个常量。
比如下面的代码中用#define定义标识符,在预处理之后,代码中的red都被替换成5,#define已经不在了。

#define red  5
int num=red;
printf("%d",num);

五、联合体

联合体类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间,所以也叫共用体。联合体也是一种特殊的自定义类型。
联合体的大小:联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

union um {
	int i;
	char q[5];
};

上面的代码中最大对齐数是4,char q[5]所占用的空间是5个字节,对齐数的大小看的是数据类型,所以他的对齐数是1,要保证联合体的大小是最大对齐数的整数倍,也就是4个整数倍,就是8。

union um {
	int i;
	short s[5];
};

这段代码中的i的对齐数是4,short s[5]所占的空间大小是10个字节,short类型的大小是2个字节,所以short s[5]的对齐数是2,要保证联合体的大小是最大对齐数的整数倍,也就是4个整数倍,就是12。
我们可以通过下面的代码了解一下联合体是如何共用一块空间的。

#include <stdio.h>
union um {
	char ch;
	int i;
	double d;
};
int main()
{
	union um un;
	printf("%d\n", sizeof(union um));
	printf("%p\n", &(un.ch));
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.d));
	return 0;
}

解析:输出结果为8 003FFE28 003FFE28 003FFE28。因为联合体共用一块空间,所以他们的地址相同,并且会被覆盖。在这个联合体中最大成员的大小就是最大对齐数的整数倍,就是8。

lianheti

联合体的运用
我们之前是通过强制类型转换判断大小端的,代码如下:

#include <stdio.h>
int main()
{
	int num = 1;
	char* ch = (char*)(&num);
	if(*ch == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

我们还可以利用联合体巧妙的解决如何判断大小端问题。

#include <stdio.h>
union um {
	int i;
	char q;
};
int main()
{
	union um un;
	un.i = 1;
	if (un.q == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

利用联合体共用一块空间的性质,先将i赋值1之后,再通过char类型的变量一个字节一个字节取出来进行判断,如果是小端存储,un.q==1就成立,反之,则不成立。

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不 良

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

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

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

打赏作者

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

抵扣说明:

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

余额充值