【C语言必学知识点六】自定义类型——联合体与枚举

封面

导读

大家好,很高兴又和大家见面啦!!!

在上一篇内容中,我们详细介绍了什么是内存对齐,以及结构体中的内存对齐以及位段的相关内容。

对于自定义类型而言,为了保证程序在运行过程中的高效性与可移植性,因此,自定义类型中的成员变量在进行内存分配时都需要遵循一定的规则。因此为了计算不同自定义类型在内存空间中的大小,我们需要了解其在内存空间中的分配规则。

在自定义类型中并不是只有结构体这一种自定义类型,还有联合体与枚举这两种类型,那什么是联合体,什么又是枚举呢?在今天的内容中我们将来对这两种自定义类型进行探讨;

一、联合体

联合体与结构体一样,也是一个或多个成员的集合,这些成员的数据类型可以不相同。但是与结构体不同的是在联合体中,这些成员是共用同一个内存空间,因此联合体也可以被称为共用体

1.1 联合体的声明

联合体的声明格式如下所示:

union tag {
	member_list;
}variable_list;
//union——联合体关键字
//tag——联合体标签名
//member_list——联合体成员列表
//variable_list——联合体变量列表

可以看到,联合体的声明与结构体不同的是关键字不同,联合体是通过关键字union进行声明,而结构体则是通过struct进行声明。

1.2 联合体中的内存对齐

与结构体一样,在联合体中同样也存在内存对齐,只不过联合体中的内存对齐与结构体相比还是会有一些区别,那么具体的区别在哪里呢?下面我们通过一个例子来看一下:

联合体
在这个例子中我们可以看到,在同样类型与数量成员的结构体与联合体中,它们被分配的内存空间确实不相同的,对于上述例子中的一个char类型的成员和一个int类型的成员:

  • 结构体根据内存对齐的规则,在VS默认对齐数为8的环境下,需要分配8个字节大小的空间;
  • 联合体根据内存对齐的规则,在VS默认对齐数为8的环境下,只需要分配4个字节大小的空间;

结构体的内存分配这里我就不再过多赘述,不太清楚的朋友可以回顾一下上一篇的内容【内存对齐与位段】。下面我们要重点探讨的是联合体的内存对齐;

首先我们通过联合体成员的地址来观察一下它们在内存中的位置关系:

联合体2

从打印的结果中我们不难发现,联合体中的两个成员的起始地址与变量的起始地址是同一个地址,也就是说,它们在内存中的情况应该是重叠的,如下所示:

联合体3

在结构体的内存对齐中我们有介绍过,对于结构体的第一个成员,会默认将其对齐到偏移量为0的地址处,也就是第一个元素的地址也是结构体变量的起始地址,而从第二个成员开始,每个成员都会对齐到偏移量为对齐数整数倍的地址处。

从这个例子中我们不难发现,在联合体中第一个成员的对齐地址处理与结构体中是一致的,都是会对其到偏移量为0的地址处,但是第二个成员居然同样也是在偏移量为0的地址处,这是为什么呢?

这个问题的答案就是在联合体的别名上——共用体。这里我们就需要理解共用体,到底共用的是什么?大家可以停留3秒简单的思考一下。

没错,共用体共用的是内存空间。

规则1:联合的大小至少是最大成员的大小。

在联合体中,我们在创建一个联合体变量时,该变量只会在内存空间中申请一个至少能够放下最大的成员的空间,并且所有的成员都会共用这一块内存空间。

因此对于t1这个联合体变量而言,它的两个成员一个是char,一个是int,我们知道char只占1个字节,而int在32为系统下需要占用4个字节,那么对于这两个类型的成员而言,最大的成员就是int类型的成员b,因此联合体变量会向内存空间申请4个字节的空间,而成员a则会与b共用同一个内存空间。

那是不是对于联合体而言,它的大小就正好是最大成员的大小呢?下面我们来看下面这个例子:

联合体4
在这个例子中我们可以看到,对于联合体union test3来说,它的最大成员应该是占5个字节的成员a,但是该联合体变量却在内存空间中申请了8个字节的空间,这又是为什么呢?

这是因为联合体变量在申请空间时,同样是根据联合体的最大对齐数来申请空间的。

规则2:联合体的大小为最大对齐数的整数倍

在联合体中它除了要能够存放最大的成员外,它同时还要满足空间大小为最大对齐数的整数倍,而对于数组类型的成员来说,其对齐数为单个元素的数据类型所占空间大小。

在这个例子中,成员a的单个元素为char类型,也就是说它对应的对齐数应该是1,而成员b的对齐数为4,也就是说该联合体的最大对齐数为4。那么该联合体既要能够放下占5个字节的成员a,又要是4的整数倍,因此该联合体的大小就是大于5且为4的整数倍的8。

下面有朋友又会有新的疑问了,如果在联合体中,成员的数量超过2个了又应该如何处理呢?如下所示:

联合体5
在这两个例子中我们可以看到,不管联合体中有几个成员,也不管第一个成员所占空间的大小是多少,联合体中的所有成员都是从联合体的起始位置进行空间分配,这就是为什么在t4t5char类型的成员存储的都是同一个内容。

正因为联合体的这种空间分配规则,所以联合体的大小就是联合体成员最大对齐数的整数倍。

现在我们对联合体有了一个基本的认识了,下面我们就来看看他跟结构体有哪些异同点:

1.3 联合体与结构体

1.3.1 相同点

  1. 性质相同——结构体、联合体都是一些成员的集合,这些成员的数据类型可以不同;
  2. 声明格式相同——结构体、联合体的声明格式都是关键字+标签名+成员列表+变量列表:
keyword tag{
	member_list;
}variable_list;
  1. 内存对齐规则相同——结构体、联合体的对齐规则都是最大对齐数的整数倍

1.3.2 不同点

  1. 关键字不同:
    • 在结构体中,是通过关键字struct进行声明;
    • 在联合体中,是通过关键字union进行声明;
  2. 内存分配不同:
    • 在结构体中,每个成员都有其各自的内存空间分配;
    • 在联合体中,所有成员共用同一个内存空间,且每个成员在使用该空间时,都是从起始位置开始
  3. 成员不同:
    • 在结构体中,每个成员都是存储的一个值,当一个成员的值被修改时,其它成员不会被影响
    • 在联合体中,成员共用同一块存储空间,因此当一个成员的值被修改时,其它成员的值也会变

1.4 联合体的使用

对于联合体这种共用内存的特性,很多朋友刚开始接触时会觉得很懵,这个联合体到底有啥用啊?我这好端端的,为啥用让几个成员共用一块内存空间呢?下面我们就来看个例子:

联合体6
在这个例子中,我们创建了一个联合体变量t6,并给t6赋值了0x11223344,在t6中有一个整型变量成员a与结构体变量成员t

  • 根据联合体的内存对齐规则,变量t中的成员会与成员a共用一块内存空间;
  • 根据结构体的内存对齐规则,结构体的大小为最大对齐数的整数倍,而这里4个成员的对齐数都是1,也就是说结构体的大小就是4;
  • 因此联合体的大小也是4。

也就是说结构体中的四个成员分别与成员a的4个字节的空间进行共用,如下所示:

联合体7
现在我们就能理解了为什么在这种情况下,成员t中的4个成员bcde所存储的值各不相同。那也就是说对于一块内存空间的使用,当我们通过在联合体中加入结构体之后,我们是可以尽可能的充分利用内存空间的。那这有什么用呢?

下面我们在来设想一个场景——现在我们要制作一个网页,在该网页中我们可以通过商品的不同特性来进行商品的查找。在网页的首页,我们需要向用户展示该商品的基本属性——名字、价格。当我们选择某一个商品时,它还会继续显示对应商品的基本特性:

  • 书:作者、内容简介、出版社
  • 衣服:品牌方、衣服材质
  • 水杯:水杯材质、产地

对于这些商品的描述,换成之前,我们可能会通过结构体来实现,如下所示:

struct commodity1{
	struct book {
		char name[10];
		int price;
		char author[20];
		char snapshot[1000];
		char publishing_company[100];
	};

	struct cloth {
		char name[10];
		int price;
		char brand[20];
		char clothing_material[100];
	};

	struct cup {
		char name[10];
		int price;
		char cup_material[100];
		char place_of_origin[100];
	};
};

这种实现方式是完全可行的,但是呢,根据结构体内存对齐的规则,我们不难发现,当我们创建一个商品变量时,该变量就会在内存中申请所有商品的属性的空间。那对于书这个商品来说,很显然,衣服的描述与水杯的描述所申请的空间就是被浪费掉的,同理,对于其他商品来说,除了自己对应的描述所申请的内存空间外,其余的空间都是被浪费掉的。

那么,如果我们即想要能够完成所有商品的描述,又不想浪费空间,那应该怎么办呢?

没错,在这种情况下,联合体就是一个最好的选择。我们可以通过结构体来描述所有商品的共有属性,通过联合体来描述单一商品的属性,如下所示:

struct commodity2 {
	char name[10];
	int price;
	union {
		struct {

			char author[20];
			char snapshot[1000];
			char publishing_company[100];
		} book;

		struct {
			char brand[20];
			char clothing_material[100];
		} cloth;

		struct {
			char cup_material[100];
			char place_of_origin[100];
		} cup;
	}u;
};

下面我们就来测试一下,这两种写法之间在内存上有多大的区别,如下所示:

联合体8
从测试结果中可以看到,当我们使用联合体来描述特殊属性时,结构体的大小直接少了352个字节。从这个例子中大家能够体会到什么呢?

这个例子实际上是想告诉我们,联合体适合描述那些相互之间有冲突的事物属性,就比如这里的书、衣服、杯子的特殊属性,相互之间是有冲突的,他们并不会同时存在,因此,对于这些内容,我们就可以通过联合体来进行内存空间的节省。

当然,联合体的用法也不止这一种,对于联合体共用空间这一特性,我们还可以进行拓展。

我们知道指针类型表示的是指针在一次操作中能够操作的字节数的大小,如字符指针,一次只能操作一个字节,整型指针,一次能够操作4个字节。在联合体中,由于内存空间是共用的 ,因此我们就可以通过联合体来模拟指针,如下所示:

联合体9
从输出结果中我们可以看到,char_point与a之间共享1个字节的空间,因此当我们要修改char_point时,我们只会修改1个字节的值,同理,short_point可以修改两个字节,int_point可以修改4个字节。

也就是说,现在我们通过联合体,也能够做到按不同类型来控制不同字节的这一功能。

1.5 小结

现在我们就会对联合体的内容做一个小结:

  1. 联合体与结构体一样,也是一些成员的集合,这些成员的类型可以不同
  2. 联合体中的成员共用同一块内存空间,当一个成员的值发生了变化,所有成员的值都会发生变化
  3. 联合体中的成员在分配内存空间时都是从起始位置开始分配
  4. 联合体的大小是最大对齐数的整数倍
  5. 根据联合体的成员共用同一块内存这一特性,可以将联合体用于以下两个方面:
    • 联合体可以用于描述不同事物之间的相互冲突的属性
    • 联合体可以用于模拟指针来访问指定大小的内存空间

二、枚举

枚举就是一一列举的意思,我们将有限种情况给一一列举出来的过程就是枚举。

在生活中,枚举的应用是非常广泛的,比如各个公司在进行招聘时,就会将招聘的岗位、人数一一列举出来;再比如日历就是将一年的每一天给一一列举出来;再比如我们在进行网上购物时,当一个商品有多个不同款式时,商家也会将这些不同的款式给一一列举出来。

以上这些都是枚举在生活中的应用。接下来我们就来看看计算机中的枚举这一自定义类型应该如何声明以及如何使用;

2.1 枚举类型的声明

与其他自定义类型一样,枚举这一自定义类型也有其独属于自己的关键字——enum,我们可以通过这个关键字来声明一个枚举类型,如下所示:

enum name {
	member_list,
}variable_list;
//enum——枚举关键字
//name——枚举类型的名字
//member_list——成员列表
//variable_list——变量列表

从声明格式中我们可以看到,枚举类型的成员列表之间并不是用分号隔开,而是用逗号隔开,之后除了关键字外的其他三要素与结构体和联合体是一致的。因此我们可以得到一个结论:

  • 自定义类型是由特定的关键字声明的一种数据类型

在枚举类型中,它与其他的两种类型还不相同,枚举类型的成员列表中的成员都是常量,而结构体与联合体中的成员都是不同类型的变量,因此,枚举类型的成员我们又可以将其称为枚举常量

2.2 枚举类型的内存分配

枚举类型在内存分配上与结构体和联合体都不相同。结构体和联合体在进行内存分配时会遵循各自的内存对齐的规则,并且结构体和联合体的大小一定是最大对齐数的整数倍。而在枚举类型中,由于其成员属于被定义的一个常量,因此枚举类型的大小是固定的,如下所示:

枚举
可以看到,不管枚举类型中的成员有多少,它的大小都是4。那是不是就是说枚举类型的大小一定是4呢?

为了解答这个问题,接下来我们就需要认识一下C语言中的常量。

2.2.1 常量的分类

在C语言中有一些无法改变的量,这些量就被称为常量。我们在C语言中会接触4种类型的常量:

  1. 字面常量——如数字、字符、字符串等
  2. const修饰的常变量——通过关键字const使变量具有常量属性,无法直接对变量进行修改,但是可以通过指针来对其进行修改,因此其本质还是一个变量;
  3. #define定义的标识符常量——通过预处理指令#define来将一个标识符定义为一个常量
  4. 枚举常量——通过枚举关键字enum定义的常量成员

对于字面常量和const修饰的常变量这里我就不再展开赘述,有兴趣的朋友可以回顾【指针】篇章中的相关内容。

这里我们需要了解的是剩余的两类常量;

2.2.2 #define定义的标识符常量

#define是C语言中提供的一种预处理指令,它可以被用来定义一些未被定义的宏和标识符常量。之所以将他定义的常量称为标识符常量,就是因为它实际上是给常量赋予了某种意义,或者说是给常量取了一个别名,如下所示:

#define MAX

现在我们通过#define定义了一个标识符常量,该标识符的含义是最大值,那它具体的值是多少呢?下面我们就来测试一下:

枚举2
可以看到,此时当我们直接使用时,程序报错了,这是因为目前我们定义的MAX它仅仅是一个标识符,那这个标识符具体的作用是什么我们并没有表达完全,此时的系统是无法判断该标识符是定义的宏还是常量,因此才会出现报错。

也就是说我们仅仅只是通过#define来定义的话,我们就需要明确的表明该表示符的具体用途:

  • 当给标识符赋予一个常量值后,该标识符则变成了一个标识符常量
  • 当给标识符赋予参数以及表达式后,该标识符则变成了一个宏

如下所示:

枚举3
可以看到此时在运行程序,程序就能准确的区分哪一个是常量,哪一个是宏。

在标识符常量中,此时的这个标识符MAX就是常量100,之所以说标识符常量就是给常量取了一个别名,这是因为此时我们就可以认为100就是当前程序下的最大值,原本100只是一个普通的常量,此时我们通过这个标识符给他赋予了某种特殊的含义,这就是#define定义标识符常量的意义。

2.2.3 枚举常量

枚举常量实际上也是一种标识符常量,只不过与#define定义的标识符常量不同,枚举常量其本身就是一个具有特殊意义的常量值,如下所示:

枚举4
可以看到,这些枚举常量会根据其在枚举类型中的先后顺序依次被赋予一个从0开始的初始值,也就是说枚举类型中的成员在声明时就已经是一个具有特殊意义的常量值了。

并且枚举类型中的常量值与#define定义的标识符常量一样,在定义时,具体的值是可以进行修改的,如下所示:

枚举5
可以看到,枚举常量在声明时如果被赋予了指定的值,那它的下一个成员如果没有指定初始值,则默认的初始值为该成员被指定的初始值+1。

那也就是说枚举类型实际上是一些具有特殊意义的常量值的集合。那么现在问题来了,枚举常量中的常量值可以是常量字符串吗?下面我们接着测试:

枚举6
从测试结果中可以看到,对于枚举常量而言,它的值只能够是整型常量表达式,因此,枚举类型实际上是由一些具有特殊意义的整型常量值组成的集合。

那么既然整型常量值,那也就是说在32位环境中,所有成员所占内存空间大小均为4个字节。这也并不能够说明为什么枚举类型的大小只有4个字节呀?为了更好的解答前面我们提出的问题,接下来我们就来看一下枚举类型是如何使用的;

2.3 枚举类型的使用

在结构体和联合体中,结构体变量与联合体变量在创建时,可以通过结构体成员访问操作符来访问结构体或联合体中的各个成员,那枚举类型的变量是否是这样呢?下面我们就来测试一下:

枚举7
可以看到,在枚举变量中,我们是无法通过结构体成员访问操作符来访问枚举成员的,那枚举常量有应该如何使用呢?

其实枚举常量的使用就是像字面常量一样,来给变量进行赋值或者直接访问的,如下所示:

枚举8
可以看到当我们将枚举成员赋值给枚举变量时,程序是能够正常运行的。这也就是枚举常量的使用方式——1.给枚举变量赋值,2.直接访问。

那对于枚举类型而言,它则是可以用于声明一些具有特殊含义的枚举常量,也可以创建对应的枚举变量。

现在可能有朋友会好奇,既然我们可以将枚举常量的值赋值给枚举变量,而枚举常量又是一个整型常量,那是不是说我们同样可以将整型常量赋值给枚举变量呢?

这个问题问的非常好,这个问题的答案在理论上来说是不行的,对于枚举变量而言,它只能够存放同类型的值,而整型常量与枚举常量是两种不同的类型,因此整型常量值并不能赋值给枚举变量。

但是为什么是理论上来说呢?

这是因为在C语言中,枚举常量的解释不够严谨,C语言跟多的是把枚举常量看做是整型常量的一种,因此,在C语言中是可以进行将整型常量赋值给枚举变量的操作的,如下所示:

枚举9
可以看到枚举此时程序是不会报错的。但是在CPP中,枚举常量则是被视为枚举类型的常量,它是不同于整型常量的,因此cpp中是无法执行整型常量的赋值操作的,如下所示:

枚举10
因此,大家不管是在哪一种环境下,尽量使用枚举常量来给枚举变量进行赋值。

2.4 枚举类型的个人理解

现在我们再回到前面的问题,枚举类型的大小是否一定是4?

现在来看这个问题,那答案是肯定的,枚举类型的大小一定是4。对于这个问题我个人是这样理解的:

  • 对于枚举类型而言,它并不像结构体或者联合体那样,在创建变量时可以通过成员访问操作符来访问其结构体或联合体成员,枚举类型的成员也就是枚举常量是通过赋值的方式来进行使用,因此对于一个枚举变量来说,它能够获取的只有一个枚举常量。
  • 对于枚举常量而言,它一定是一个整型常量,因此枚举常量所占空间大小为4

基于以上两点,我们不难得知枚举类型在使用时实际上只需要申请一个整型空间即可,因此枚举类型的大小一定是一个整型常量的大小。在32位系统下,整型所占空间大小为4,因此枚举常量在32位系统下的大小一定是4.

2.5 小结

现在我们也已经介绍完了枚举类型的全部内容,下面我们就来对枚举类型做一个小结:

  1. 枚举类型是通过枚举关键字enum进行声明的一种自定义类型;
  2. 枚举类型可以看做是一些有特殊含义的整型常量的集合;
  3. 枚举类型的大小为一个整型常量的大小;
  4. 枚举类型可以用于声明具有特殊含义的枚举常量;
  5. 枚举类型可以用于创建能够存储枚举常量的枚举变量;
  6. 枚举常量可以进行直接访问,也可以用于给枚举变量进行赋值操作;
  7. 枚举常量的值默认从0开始依次递增;
  8. 当一个枚举常量被赋予了指定的初始值后,该成员的下一个成员的初始值默认是该成员的值+1

结语

今天的内容到这里就全部结束了,在下一篇内容中我们将介绍《动态内存管理》的相关内容,大家记得关注哦!如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。最后感谢各位朋友的支持,咱们下一篇再见!!!

评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值