前言
我们都知道,C语言为了方便我们描述一些数据类型,提供了诸如:char、short、int、long、float、double等内置类型,通过这些内置类型我们可以将数据记录下来,但我们用C语言除了描述数据之外我们往往还会面临着描述:一本书、一个学生,包括他的年龄、身高、体重等等,我们发现光靠这些内置类型是不足以达到我们的目的,于是为了解决这些问题,C语言提供了自定义类型,我们今天要讨论的结构体、联合体和枚举就是自定义类型。
一、结构体
首先我们要明确什么是结构体?结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。如:数组、指针甚至是其他结构体。那么结构体是怎么声明的呢?我们又该如何使用结构体?
结构体的关键字是struct,声明如下:
举个例子,我们要描述一个学生:
此外,结构体是一种变量,我们知道,变量是需要初始化的,那么结构体变量是如何初始化的呢?我们展开以下讨论。
1.1 结构体变量的初始化
在介绍结构体变量的初始化之前,我想先向大家介绍结构体访问操作符” . “ 以及” -> “。
结构体成员是分为直接访问和间接访问的,用.成员名 的方式是直接访问,用->成员名访问是间接访问,是针对于结构体指针的。
有了上述知识的铺垫我们针对上面创建的结构体进行初始化。
1.顺序初始化
2. 指定顺序初始化
3.结构体成员的间接访问
1.2 结构体的特殊声明
我们在结构体声明的时候可以不完全声明,即:匿名结构体,例如:
但值得一提的是,匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
1.3 结构体的自引用
什么是自引用?就是在结构中包含一个类型为该结构本身的成员,这个成员被叫做链表的一个节点,后续通过这个成员可以访问下一个节点。那么我们该如何使用呢?请看以下代码:
struct Node
{
int data;
struct Node* next;
};
注:结构体成员的自引用只能通过结构体指针的方式。
1.3 结构体的内存对齐
在我们了解结构体的基本应用后,我们来探讨一个问题,计算结构体成员的大小。我们先看以下代码:
我们看到,两个顺序不同但成员完全一样的两个结构体,计算出来的长度是不一致的,这是为什么呢?实际上,这里就涉及结构体成员的对齐。
结构体对齐的规则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到对齐数的整数倍的地址处。对齐数=编译器默认的对齐数与该成员变量大小的较小值(vs默认对齐数为8;Linux中gcc没有默认对齐数)
3.结构体的总大小为最大对齐数的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍。
了解了对齐规则后我们再来分析上面的代码:
我们看到,对于第一个结构体,s1的char类型被放在了偏移量为0的位置,随后在偏移量为1的位置将s2 的char类型放了上去,随后对于s3,由于它是int类型占四个字节,所以应该在偏移量为4的整数倍的位置开始存放,于是s3就从偏移量为4的位置开始向后存放四个字节,将它们都存放好后,我们看到整个结构体占了8个字节,而第一个结构体的最大对齐数是4,8刚好是4的整数倍,因此第一个结构体的大小是8个字节。
对于第二个结构体,首先将s1放在偏移量为0的位置,随后,由于s3的大小是四个字节,因此浪费三个字节的空间,s3应当从偏移量为4的位置开始存储,向后存储四个字节的空间,由于s2的对齐数是1,因此将s2储存在s3的后一位,将所有的成员存储完毕后计算结构体的大小为9个字节,而第二个结构体的最大对齐数是4,所以还需浪费三个字节的空间,保证结构体的大小为最大对齐数的整数倍,因此第二个结构体的大小是12个字节。
在讨论完结构体的内存对齐后,我们不禁疑惑,为什么要内存对齐呢?大部分参考资料是这么说的:
1.平台原因:不是所有的硬件平台都能访问任意地址的数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构应尽可能地在自然边界上对齐。原因在于为了访问未对齐地内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次。例如:一个处理器总是从内存中取8个字节,则地址必须是8的倍数,如果我们保证将所有的double类型的数据地址对齐到8的倍数,那么就可以用一次内存操作来读或者写值了。
总体来说:结构体的内存对齐是拿空间换取时间的做法。
那么如何做到既节省空间又保证效率呢?让占用空间小的成员尽量集中在一起。
1.4 结构体实现位段
什么是位段?实际上,位段就是结构体以比特位的形式定义结构体成员开辟空间的大小。
怎么定义位段呢?例如:
像这种结构体成员名后面以:数字的形式就是位段。位段的出现就是为了节省空间,它的成员必须是int、unsigned int或signed interesting,在C99中位段成员可以是其他类型。
那么位段所占的内存是多少呢?
我们可以看到上述位段的内存大小是四个字节,这与结构体的内存对齐是完全不同的,那么它是如何实现内存分配的呢?我们将数据类型变为char再来测试:
我们知道,一个char类型变量的大小是1个字节,也就是8比特位,当后面的数字之和小于等于8时位段的大小为1个字节,当大于8小于等于16时位段的大小为2个字节。那么我们就可以试着分析位段的内存分配:
就第二个代码而言,
我们看到,对于位段而言,前面的数据类型是开辟空间的方式,一次开辟一个字节,int一次开辟四个字节,对于vs2022而言是从右向左依次使用,如果剩余空间不够下一个成员使用就浪费,重新开辟一块新的空间存储下一个成员。
位段的跨平台问题:
1.int 位段被当成有符号还是无符号数是不确定的;
2.位段中最大位的数目不能确定。(32位机器最大是32,16位机器最大是16)
3.位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
二、联合体
联合体的关键字是union,与结构体一样,都是由一个或者多个成员构成,这些成员可以是不同的类型。但是编译器只为最大的成员分配足够的空间。联合体的特点是所有成员共用一块空间,所以联合体也叫共用体。给联合体其中的一个成员赋值,其他成员的值也会发生变化。那么联合体是如何使用的呢?请看以下代码:
union S
{
char a;
int b;
};
int main()
{
union S p = { 0 };
printf("%d\n",sizeof(p));
return 0;
}
联合体的特点:
我们看到,联合体的所有成员的地址指向同一块空间,其大小至少是最大成员的大小 。
2.1 联合体的大小计算
1.联合体的大小至少是最大成员的大小。
2.当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
也就是说联合体也存在内存对齐,它并不是简简单单地等于最大成员的内存大小。
例如:
分析上述代码,a占五个字节,而最大对齐数是4,因此内存大小应当对齐到四的整数倍,也就是8,所以上述联合体的大小是8个字节。
2.2 联合体的作用
例如,当我们在描述不同的商品时存在很多不同的信息,但总会有一些公共类型的数据,我们将着些公共类型的数据用联合体描述,当我们要描述这些公共类型的数据时我们就用联合体调用,假如我们不使用联合体,而是用结构体将每一件商品的各种信息都列出来,这样不仅提高了代码量,而且最重要的是造成了空间浪费,因此结构体最重要的作用就是节省空间。
三、枚举
枚举顾名思义就是一一列举,它的关键字是enum,例如,我们在描述星期时可以使用枚举:
enum Day
{
Mon, Tues, Wed, Thur, Fri, Sat, Sun
};
{}中的内容是枚举类型的可能值,也叫枚举常量。这些可能取值是由默认的初始值的,从0开始,依次递增1,也可以主动给枚举常量赋值。
但是枚举的出现仅仅是赋值吗?如果是的话#define定义的常量也可以赋值,为什么还要用枚举呢?事实上,枚举的出现增加了代码的可读性,通过枚举给枚举常量赋值,后续代码在用到枚举类型的值时可以直接将枚举类型写入代码,其他人在阅读时可以更好地理解代码的意义;其次它比#define定义的标识符对比更加严谨;使用方便,依次可以定义多个常量;枚举常量遵循作用域规则。
以上就是本篇博客的全部内容,受限于博主的知识水平,可能文章中有些许不足,欢迎大家指正。如果对您有帮助的话记得点赞收藏加关注,您的点赞就是对我最大的鼓励