C/C++学习之路之自定义类型:结构体、枚举、联合

前言:在C语言中,不仅有内置的数据类型,还有一些自定义类型,像结构体、枚举、联合。所以今天博主带你详细地学习这些自定义类型。(看完如果对你有帮助,点个赞谢谢啦!)

目录

一、结构体

1.1 结构的声明

1.2 结构体变量的定义与初始化

 1.3 结构体的自引用

​编辑 1.4 结构体内存对齐

1.5 修改默认对齐数

1.6 结构体传参

二、位段

2.1 什么是位段

2.2 位段的内存分配

2.3 位段的跨平台问题

三、枚举

3.1 枚举类型的定义

3.2 枚举的优点

3.3 枚举的使用

四、联合(共用体)

4.1 联合类型的定义

4.2 联合的特点

4.3 联合大小的计算


一、结构体

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

举个例子。有一个身份:学生,学生身份中包含了一些信息:学生的名字,学生的年纪,学生的学号等。把这些用结构体表示,就是:有一个结构体类型,这个类型的名字是学生,这个类型包含了一些成员变量,如名字(用字符数组存储)、年龄(用int存储)、学号(用字符数组存储)。

	struct stu  //学生
	{
		char name[20];  //名字
		int age;  //年龄
		char number[20];  //学号
	};

现在有一个学生,名字叫张三,年龄20,学号是052203216,转换成结构体就是,有一个“学生类型”的变量,变量名为A,该变量的值为:name[20]="zhangsan",age=20,number[20]=052203216

	struct stu A = { "zhangsan",20,"052203216" };

这样我们就知道学生A的名字、年龄、学号。打印出来就是:

那接下来就让博主带你一步步了解结构体。

1.1 结构的声明

结构体类型声明的语法形式如下:

struct  类型名

{
        成员变量A;

};

·类型名就是你想要定义的结构体类型的名字

·成员变量的格式就相当于一个变量的声明,且结构体内部的成员变量个数不受限

·大括号后记得加分号

(具体使用请参考上面的第一段代码)

除了上面的正常的声明,还有一种特殊的结构体声明,即在声明结构体时省略了类型名,称为匿名结构体类型,其声明格式如下:

struct  

{
        成员变量A;

};

匿名结构体比较特殊的地方在于,它的变量仅能在匿名结构体声明的时候定义(变量的定义下面会讲)。

值得注意的是,虽然我们可以重复声明匿名结构体,但是编译器是会把这些匿名结构体当作不同的类型,如:

其中,变量AB与变量CD就属于不同的类型,所以最下面的语句,也就是用指针pcd存储变量AB的地址,是非法的!

1.2 结构体变量的定义与初始化

结构体变量的定义有两种方法,一种是在声明的结构时同时定义该类型的变量:在大括号外加上要定义的变量,如下图:

	struct stu  //学生
	{
		char name[20];  //名字
		int age;  //年龄
		char number[20];  //学号
	}B,C;
    //多个变量之间用逗号隔开

一种就是跟其他类型一样:

    struct stu D;

注意!结构体类型,是struct类型名共同组成,使用时不要忘了!

讲完了声明,下一步就是初始化了。

最基本的初始化就是,用大括号把值括起来,在大括号里面按成员变量的顺序把初始化的值存储起来,类似于数组,看下图:

    struct stu A = { "zhangsan",20,"052203216" };

还有一种不按成员变量顺序进行的初始化:

	struct stu D = { .number = "052200316",.name = "lisi",.age = 21 };

这就相当于在大括号内赋值,只是省略了变量名。大括号内赋值语句用逗号隔开。

补充:结构体变量的赋值格式有两种:

1.结构体变量名.成员变量名

2.结构体变量指针->成员变量名

 1.3 结构体的自引用

 对上面的内容,博主先补充一点:结构体的成员变量也能包含其他的结构体类型。例如:

那结构体的成员变量能不能是自身的结构体类型呢?

答案是不行的。原因是结构体类型大小是未知的。我们事先将一个结构体类型(假设为A类型)声明完成,此时我们已经知道了A类型的大小,当我们在另一个结构体类型(假设为B类型)包含A时,此时B类型的大小就为A类型加上其他成员变量的大小(实际上结构体类型的大小没这么简单,等下会讲)。而如果B类型包含自身,但自身大小未知,最后的结果还是无法确定B类型的大小。

但想要做到自引用是可以实现的,只是我们要用另一种方式:指针。

相对于结构体类型,指针类型大小一定是4或8个字节(取决于编译器位数),我们只要引用自身结构体的指针类型,就可以保证结构体类型的大小是确定的。如:

	typedef struct LNode* List;
	struct LNode
	{
		int Data;
		List Next;
	};

应用如下(链表):

 1.4 结构体内存对齐

上面我们提到,结构体类型不是将成员变量的大小相加那么简单。现在博主就来详细地讲解一下结构体类型大小计算。

结构体类型大小的计算,其核心为结构体内存对齐,以下为结构体的内存规则:

1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。(VS中默认的值为8)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

接下来就让博主一条条地解释这些规则。

首先我们先举个例子:

计算过程如下:

其中p指向结构体内存的第一个字节的地址,旁边的数字则是相对于第一个字节的偏移量。则根据对齐规则的第一条,我们的第一个成员变量a就从0开始开辟,占据的字节如下:

对于第二个成员变量b的开辟,根据第二条规则,编译器(博主用的为VS)的默认对齐数为8,成员变量b的类型为int,大小为4,则取两者中最小的4为对齐数。也就是说,我们第二个变量的开辟,要从4的整数倍的位置开始开辟。此时可开辟的最小的位置为4,那我们就从4那个位置开辟4个字节。

同理,对于成员变量c,我们得到对齐数2,此时可开辟的最小的位置为8,开辟两个字节,得到:

再根据规则三,a的对齐数为1,b的对齐数为4,c的对齐数为2,取最大的4,再从4的整数倍中找到能包含以上空间的数,也就是12个字节,作为结构体的大小。

而理解了前三条规则,第四条也就能明白了,这里简单说明以下即可。

在这个例子中,a从0开始,开辟一个字节。b为结构体变量,由上个例子可知,b大小为12,对齐数为其成员变量的最大对齐数,也就是4,所以从4开始,开辟12个字节。c对齐数为1,直接从16开始,开辟一个字节。d的对齐数为2,从2的整数倍18开始,开辟两个字节。最后,在exm2中的四个成员变量中,对齐数最大的为4,则exm2大小为4的倍数,也就是20。

到这里,博主就将结构体内存对齐的知识讲完了。结构体内存对齐可以帮助我们计算结构体变量的大小,是特别重要的知识点,希望读者可以熟练掌握。

为什么存在结构体内存对齐?大部分参考资料是这么说的:

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

而在设计结构体的时候,我们最好让占用空间小的变量尽量集中在一起,这样会比较节省空间。

1.5 修改默认对齐数

我们可以通过#pragma这个预处理指令修改默认对齐数。例如:

1.6 结构体传参

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

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

二、位段

2.1 什么是位段

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int,unsigned int或signed int。(在C99之后也可以是其他类型,但主要还是以int和char为主)。
2.位段的成员名后边有一个冒号和一个数字。

例如:

	struct exm3
	{
		char a : 2;
		char b : 4;
		int c : 6;
		int d : 8;
	};

 位段的位指的是二进制位,而成员变量后面的数字的意义是该成员变量占据多少二进制位,也就是在空间中占据多少比特。

2.2 位段的内存分配

上面我们讲到,位段的成员变量的开辟以比特为单位。现在让博主来讲解位段的内存分配具体是什么样的。

我们以struct exm3类型的变量ww为例

第一个成员变量a的类型为char,则内存中先开辟一个字节。a后面的数字为2,表示a占据两个比特位,一个字节有八个比特位,将最低的两个比特分配给a。具体情况如图:

第二个成员变量b的后面的数字为4,表示b要占据四个比特。在我们第一个开辟的字节中,还剩下6个比特,可以放下变量b,则将a前面的4个比特分配个b。如图:

第三个成员变量c后面的数字为6,第一个字节已经放不下了,并且c的类型为int,则我们这次要开辟四个字节。根据结构体内存对齐,这次int开辟的位置的偏移量为4。开辟情况如图:

因为c占据6个比特,d占据8个比特,则他们的内存分配如图:

最后的内存分配为:

则struct exm3的大小就为8个字节。

而我们也对ww的成员变量进行赋值,现在就来分析一下ww的内存中的值。

数据在内存中是以补码的形式存储,a的补码为00000010,取后面两位(a占据2个比特位)存入内存中,b补码为00000011,c补码为00000001,d补码为00000001,分别根据每个变量占据的比特将数据存入,最后得到:

最后大概地总结一下就是,位段的开辟会根据成员变量的类型来决定开辟的大小。当上一次开辟后的空间能存下下一个成员变量,则无需开辟空间。若不够,则根据下一个成员变量的类型开辟空间并存放变量。并且,位段的内存开辟也具有结构体的特点。

2.3 位段的跨平台问题

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

总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

三、枚举

枚举的意思为一一列举。例如,一周包含星期一到星期日,这七天就可以一一列举:星期一、星期二、星期三...星期日。而枚举类型,就指的是变量的值可以列举。

3.1 枚举类型的定义

	enum Day//星期
	{
		MON,//注意是逗号隔开
		TUES,
		WED,
		THUR,
		FRI,
		SAT,
		SUN  //最后的枚举变量不需要逗号
	};//最后用分号

以星期为例,其中的Day就是枚举类型名,enum Day就是一个枚举类型。{}其中的MON、TUES等是枚举类型的可能取值,也叫枚举常量。

这些枚举变量都是有值的,若像上面那样没有赋值,那第一个枚举变量默认为0,后面的依次加1。我们也可以自己赋值,如:

	enum Day
	{
		MON,
		TUES,
		WED=3,
		THUR,
		FRI,
		SAT=7,
		SUN
	};

像这样对个别枚举常量进行赋值,那后面的常量也是依次加1。这里的MON、TUES还是0和1,WED被赋值为3,后面的枚举常量一次加1,THUR=4,FRI=5,SAT被赋值为7,SUN=8。

也可以完全赋值,如:

	enum Day
	{
		MON=1,
		TUES=4,
		WED=3,
		THUR=5,
		FRI=2,
		SAT=7,
		SUN=9
	};

3.2 枚举的优点

既然我们可以用#define定义常量,为什么要使用枚举?

这是因为枚举有以下优点:

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

3.3 枚举的使用

如下图:

还有一些特殊的使用:

这样的用法虽然编译器没有报错,但这么用没什么意义。既然你定义了枚举类型又不使用枚举常量,那这个枚举类型就失去了意义,代码的可读性就比较差。

补充:枚举类型的大小与int相同,与枚举常量的个数无关。

四、联合(共用体)

4.1 联合类型的定义

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

//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;

联合类型的声明及其变量的定义与结构体类似,就不多赘述了。

4.2 联合的特点

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

什么意思呢?我们以刚才声明的联合类型union Un举例。Un里面有两个成员变量,一个是char类型的c,一个是int类型的i,它们所占的空间分别是1和4个字节。而由于联合的特点,此时的c和i是处于同一块空间的,它们占据的字节如图:

由图可知,此时成员变量c和i的地址与其变量的地址是相同的。并且,由于成员变量占据的是同一块空间,则成员变量的值是相关的。这一点可以用来检验机器的大小端。如:

int check()
{
	union Un
	{
		char c;
		int i;
	}un;
	un.i = 1;
	return un.c;
}

这种方法检验的原理是:我们把成员变量i赋值1,1的补码为00 00 00 01(十六进制表示),如果机器为小端存储模式,则补码中的低位01存储在地址最低的那个字节,而un.c就能访问那个字节。则un.c如果为1,表明机器为小端存储模式;若为0,则表示机器为大端存储模式。如图:

 

4.3 联合大小的计算

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

举个例子:

在这个例子中,c对齐数为1,i的对齐数为4,则该联合的大小就应该是4的倍数,且要大于5,则union A的大小就为8。

五、结语

对于这些自定义类型,我们需要熟练掌握各种类型的声明及其特点,对于一些较难的知识点,如结构体的内存对齐,就需要我们多做一些例题,熟能生巧,之后再进行计算就挺快的了。

以上就是《C/C++学习之路之自定义类型:结构体、枚举、联合》的所有内容,如果对你有帮助,点个赞支持一下博主吧!若有不足之处,也希望各位能够在评论区指出,每一个建议我都会虚心听取。

谢谢观看!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值