自定义类型详解:结构体 (内存对齐)+ 枚举 +联合体

✨✨✨学习的道路很枯燥,希望我们能并肩走下来!

编程真是一件很奇妙的东西。你只是浅尝辄止,那么只会觉得枯燥乏味,像对待任务似的应付它。但你如果深入探索,就会发现其中的奇妙,了解许多所不知道的原理。知识的力量让你沉醉,甘愿深陷其中并发现宝藏。


前言

我们之前都简单的了解过结构体,但并没更进一步的了解,本篇通过讲解结构体,枚举,联合体进一步加强对自定义类型的使用与理解,如有错误,请在评论区指正,让我们一起交流,共同进步!


1.结构体

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

1.1结构体的声明

1.1.1结构体基本结构

=》结构体关键字struct ;
=》结构体标签名 tag =》根据自己的实际情况为自定义类型命名
=》结构体成员列表 member- list =>可以是1个成员或多个成员
(成员可以是不同的类型,char,double,结构体等等)
=》结构体变量名 variable - list (不一定要声明变量,可以没有): 可以在定义结构体直接声明,也可以在主函数中命名; 这里在定义的时候直接声明的变量,是全局变量。我们一般推荐在主函数中通过类型struct tag声明变量。
注意:分号不可以丢!!!

struct tag  
{
	 member-list;
}variable-list ; //这里的分号不能丢

1.1.2自定义结构体实现

假如我们要定义一本书的类型:其中要包含书名,作者,价格,编号
我们C语言所学的基本类型肯定就满足不了了,这时候我们通过自定义结构体实现。
自定义的结构体类型相当于int ,double ,float等类型
struct book=>就是书的类型
代码如下:

#include<stdio.h>
struct book
{
	char book_name[20];
	char author[20];
	int price;
	char id[15];
}sb3,sb4; //=》全局变量

int main()
{
	struct book sb1;
	struct book sb2;//局部变量
	return 0;
}

1.1.3结构体的不完全声明(匿名结构体)

使用匿名结构体必须在声明的时候,就声明结构体变量,而且匿名结构体只能使用一次。
在没有标签的结构体,不能在主函数中通过类型声明变量。
例如(错误演示):struct sb1; //error

#include<stdio.h>
struct 
{
	char book_name[20];
	char author[20];
	int price;
	char id[15];
}sb1,sb2; //=》全局变量

int main()
{
	return 0;
}

省略掉标签名后,下面代码合理吗?我们想一下?

#include<stdio.h>
struct 
{
	char book_name[20];
	char author[20];
	int price;
	char id[15];
}sb1;

struct 
{
	char book_name[20];
	char author[20];
	int price;
	char id[15];
}* p;
int main()
{
	p = &sb1;
	return 0;
}

尽管上述代码的成员变量一样,但上述代码是非法的,编译器会把他们当成两个完全不同的类型并发生警告。所以我们需要注意!!!

1.2结构体的自引用

结构体的自引用,我们在数据结构中常见,线性数据结构(顺序表,链表),树形数据结构 (二叉树)。
我们怎么把链表连接起来呢,我们就用了自引用,我们自己创建的结构体(结点)包含存储的数据 (data),指向下一个结点的指针。

为什么要用指针呢?
解释如下:

//我们的初步想法:结构体里面存储一个结构体,意思想要找到下一个结构体
//下面无法计算出sizeof(struct Node)
//错误示范,这样的书写没有办法算出结构体多大,并且会无限套娃使编译器出现错误
struct Node
{
	int data;
	struct Node n;
};

//正确自引用 通过指针找到下一个结点
struct Node
{
	int data;
	struct Node* nest;
};

问题又来了,struct Node 这样的类型太长了,我们能简化一些吗?
所以我们可以使用 typedef 重新对结构体类型命名。

typedef struct Node
{
	int data;
	struct Node* nest;
}Node; =》结构体名称Node

为了偷懒有人这样写

typedef struct Node
{
	int data;
	Node* nest;
}Node; =》结构体名称Node

这样写是错误的,Node结构体名称是在重命名完后才有Node我们是不能提前使用的Node的

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

两种声明:定义时顺带声明,或者在主函数中使用结构体类型声明
初始化:定义时顺带初始化,或者在主函数中使用结构体类型初始化

struct bookauthor
{
	char name[20];
	int age;
};
struct book
{
	char book_name[20];
	char author[20];
	int price;
	char id[15];
	//struct bookauthor s;  //结构体中也可以嵌套结构体,但初始化时候需要 {},初始化内嵌结构体
}sb3 = {"C语言","作者",43,"HI02110"},sb4;//声明,初始化

struct book sb5;//也可以这样声明
int main()
{
	struct book sb1 = { "《c++》","zhangsan",88,"PG1001" };//声明
	printf("%s %s %d %s\n", sb3.book_name, 
		sb3.author, sb3.price, sb3.id);
	return 0;
}

有顺序和无顺序结构体初始化

struct S
{
	char c;
	int a;
	float f;
};
int main()
{
	//有顺序
	struct S s = { 'w', 10, 3.14f };
	printf("%c %d %f\n", s.c, s.a, s.f);

	//无顺序 ,乱序初始化
	struct S s2 = { .f = 3.14f , .c = 'w', .a = 10 };
	printf("%c %d %f\n", s2.c, s2.a, s2.f);
	return 0;
}

1.4 结构体内存对齐

知道了结构体应用,那我们更进一步思考一下:如何计算结构体的大小?
我们来看以下例子:

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

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

没有接触过的小伙伴可能会认为结构体中的类型占几个字节,结构体就多大,可能会算出1+4+1=6,1+1+4=6这两个结果。
实际答案是:12,8
这涉及了我们的结构体内存对齐

1.4.1offsetof

这里使用offsetof 宏:用来计算结构体成员相对于起始位置的偏移量
使用offsetof:头文件#include<stddef.h>
两个参数:结构体,结构体成员

在这里插入图片描述

在这里插入图片描述
c1在0号位置;i在4下标位置;c2在8下标位置,内存图如下:

在这里插入图片描述

红色的空间分配给S1(S2)空间但没有使用

1.4.2结构体对齐规则

结构体的对齐规则:
1. 结构体的第一个成员直接对齐到相对于结构体变量起始为0的地址处。
2. 从第二个成员变量开始,要对齐到某个(对齐数)的整数倍的地址处。 假如0,1已经偏移完,要从2位置开始对齐,此时的成员类型int对齐数是4,我们必须找到下标为4的整倍数处开始偏移,必须跳过2,3从4开始计算。
3. 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8;Linux环境下默认不设对齐数(为成员自身大小)
4. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。每个成员都有一个对齐数,其中最大的就是最大对齐数。
5. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

计算对齐数:

在这里插入图片描述

对于嵌套结构体:嵌套的结构体对齐到自己的最大对齐数的整数倍处
例如嵌套的s3结构体需要从8的倍数开始对齐

在这里插入图片描述

为什么要内存对齐?

  • 1 .平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 2.性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
struct S1
{
	char c;
	int i;
};
 //如果不对齐,存储一个字节c,后面会紧跟 i 的四个字节,
 //当以4个字节读取时,会读取到 i 的前3个字节,
 //再次读取c时,会重复读取前三个字节,而内存对齐就避免了这样的情况

在这里插入图片描述

总结:结构体内存对齐是拿空间来换取时间的做法
所以为了设计结构体节省空间:让占用空间小的成员尽量集中在一起。例如:类似char,char集中在一起,在放其他类型

1.4.3修改默认对齐数

我们一般设置默认对齐数都是2的n次方

//括号里面放的就是默认对齐数
#pragma pack(4)
struct S1
{
	double d;
	char c;
	int i;
};

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

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

1.5 结构体传参

结构体传参使用地址传参
代码来分析使用地址传参:

struct S
{
	int data[1000];
	int num;
};
void print(struct S s)
{
	printf("%s %s %s %s\n", s.data[0], s.data[1], s.data[2], s.num);
}
void print2(struct S* ps)
{
	printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
}
int main()
{
	struct S ss = { {1,2,3,4,5},100 };
	print(ss);//值传递,浪费空间,形参是临时拷贝,拷贝空间过大就会浪费空间
	print2(&ss);//地址传递,只是4/8字节的地址,节省空间
	return 0;
}

使用地址传参原因:
1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
2.如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。

1.6 结构体实现位段

位段是基于结构体出现的,说了结构体我们再来看看位段。

1.6.1认识位段

位段的声明类似结构体,但有两个不同:
1. 位段的成员必须是 int,unsigned int 或者signed int
2. 位段的成员名后边有一个冒号和一个数字

A就是一个位段类型:位就是二进制位(比特位)

struct A
{
	int _a : 2; 
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
struct S
{
	int a;
	int b;
	int c;
	int d;
};
int main()
{
	printf("%d\n", sizeof(struct S));
	printf("%d\n", sizeof(struct A));
	return 0;
}

在这里插入图片描述

知道了位段,那它的大小又是多少?结果和你想的一样吗?
字段的使用:
结构体A中是字段,a是int类型开始先开辟4个字节(32个bit)供_a使用2个bit,剩余30个bit,_b使用5个bit,剩余25个bit,_c使用10个bit,剩余15个bit,_d要使用30个bit,剩余bit不够,就需要重新开辟,因为是int类型所以会再开辟4个字节空间(32个bit).

那么问题来了,有了新开辟的空间它还会继续使用原有剩下的bit空间吗?
我们有两种想法:
1.接着使用剩余的bit,不够了再使用新的bit空间
2.直接丢弃剩余的空间,使用新的bit空间
这间接表示了位段的不确定性。

我们来看一下位段的内存分配,进一步了解一下!

1.6.2位段的内存分配

  1. 位段的成员是int, unsigned int, signed int 或者是char 类型
  2. 位段的空间上是按照需要以4个字节或1个字节的方式开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段

例子

struct S
{
	char a:3;
	char b:4;
	char c:5;
	char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

我们通过这个例子理解一下字段:

我们首先要明白char - 1个字节 - 8个bit位
我们存放数据是从左向右,还是从右向左这是不确定的,这里我们不会从中间开始放置,这是不合理的,数据一般从低地址往高地址存放或者从高地址往低地址存放。
这里我们先假设规则①从右向左存储数据,②如果剩余空间不够存放下一个数据,我们就丢掉,根据类型重新开辟一个空间继续存放数据。

我们先创建结构体S,猜测它占多大的内存

白色的空间丢掉根据制定的规则在这里插入图片描述

根据下图我们可以知道我们的思路大概没错,但是其中的细节又是什么,我们再来研究一下!

在这里插入图片描述

我们通过图解理解位段的存储

在这里插入图片描述

1.6.3位段的跨平台问题

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

位段存在跨平台问题,使用时注意

2. 枚举

2.1枚举类型的定义

枚举 – 就是一一列举
可能的取值 -》列举
我们生活中的一周的星期是有限的7天,性别只有男女,月份只有12个月。

枚举关键字:enum
枚举标签:Day
括号里面的:可列举的值用逗号隔开,最后一个不用加逗号

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
//创建一个颜色的类型
enum Color
{
	RED,
	GREEN,
	BLUE
};
int main()
{
	enum Color color = BLUE;
	return 0;
}

颜色类型的枚举变量color它的取值只能是枚举里面包含的值(BLUE,GREEN,BLUE)它是固定的。

在这里插入图片描述

枚举的可能取值,每一个可能的取值是常量,所以大括号中的值为枚举常量。

这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值,赋值完后的枚举常量依次+1在这里插入图片描述

2.2 枚举的优点

我们可以使用#define 定义常量,但是#define没有类型,不够严谨,这就体现出枚举的优点

enum Color
{
	RED = 5,
	GREEN = 7,
	BLUE = 10
};
#define RED 5
#define GREEN 7
#define BLUE 10

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

3. 联合

3.1 联合类型的定义

联合是一种特殊的自定义类型
我们要注意类似结构体但他们是不同的。这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

union Un
{
	char c;
	int i;
	double d;
};
int main()
{
	union Un un;//联合体定义
	printf("%d\n", sizeof(union Un));//8
	printf("%d\n", sizeof(un));//8
	return 0;
}

3.2 联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

union Un
{
	char c;
	int i;
	double d;
};
int main()
{
	union Un un;
	printf("%p\n", &un);
	printf("%p\n", &(un.c));
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.d));
	return 0;
}

在这里插入图片描述

通过结果我们可以知道他们的起始地址都一样。

他们存放在空间中,如下图,为了方便看,不使用一个图表示,我们分成3个图观看,但其实是一块空间,使用时会覆盖。

在这里插入图片描述

使用联合体其中一个成员,不同时使用多个联合体成员,就可以使用联合体。

使用联合体判断大小端问题?

联合体:利用联合体特点共用一块内存
u.i =1; 将1存放在int4个字节中,小端u.c取的是01,大端取的是00
u.c只取第一个字节里的内容

int check_sys()
{
	union
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

在这里插入图片描述

回忆函数判断大小端:

int check_sys()
{
	int num = 1;//取四个字节中第一个字节
	char* p = (char*)&num;// int*=》转化char*类型
	if (*p == 1)
	{
		return 1;
	}
	else
	{
		return 0;
	}
	//简化可直接返回 return *(char*)&num;//是0或1直接返回
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

3.3 联合大小的计算

1. 联合的大小至少是最大成员的大小。
2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

union Un
{
	char arr[5];
	int i;
};
int main()
{
	printf("%d\n", sizeof(union Un));
	return 0;
}

注释:开辟的4个字节不够字符数组共用,所以需要开辟新空间共用,因为最大对齐数是4,开辟的空间必须是4的整倍数,所以开辟8个字节就够用了。(5,4是所占字节数)在这里插入图片描述

我们来练习一下吧!

union Un
{
	short a[6];// 12; 2,8=>2
	int i; // 4 ; 4,8=>4
};
int main()
{
	printf("%d\n", sizeof(union Un));
	return 0;
}

总结

✨✨✨各位读友,本篇分享到内容是否更好的让你理解了自定义类型,对结构体有了新的认识,如果对你有帮助给个👍赞鼓励一下吧!!
🎉🎉🎉一遇挫折就灰心丧气的人,永远是个失败者。而一向努力奋斗,坚韧不拔的人会走向成功。
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值