结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
我们以描述一个学生为例,
一个学生变量,用性别,年龄,姓名描述。
打印效果
匿名结构体类型
在上述代码中,并未给结构体加上标签,所以我们在使用时无法直接使用其变量,在;前创建变量,且只能用一次。
那么问题来了?
在上述代码的基础上,下面的代码是否合法?
该等式并不成立,在c语言中,虽然他们的成员变量是相同的,但是他们的结构体类型不相同,所以编译过程会报错,不同类型的成员互不兼容。
在这里,我们引入数据结构的部分内容来理解。
这个代码能否帮助我们从一个数字串联到下一个数字?
从表面上看,确实从一个结构体中能找到下一个结构体中的数据,但是
如果可以,那sizeof(struct Node)是多少?
将会是一个无穷大的量,无法计算,所以不可行。
正确的自引用方式:
这里我们如何理解呢?
这样就可以串联链表中的每一个数字,结构体引用结构体,(类似于递归),这就是结构体的自引用。
有了结构体类型,那么如何定义结构体成员变量呢?
初始化:定义变量的同时赋初值
结构体嵌套初始化
在掌握结构体的基础知识后,我们想要计算一下结构体的大小,那么是如何计算的呢?
结构体在计算大小时会出现一个问题,那就是结构体的内存对齐
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
Linux中的默认值为4
对规则的解读:
什么叫做偏移量?
让我们通过练习来熟悉内存对齐的使用
练习一
请问该结构体的大小是多少?
可能是6,也可能是其他,让我们运行一下程序。
结果为 12,那么如何得出结构体的大小为 12 呢?
char c1 占1个字节 ,从结构体的起始位置开始存储。
int i 占4个字节,vs环境下他的对齐数为4,所以他在地址中要从4的倍数开始储存。
char c2 占1个字节, 对齐数为1,所以它在int 后又占1个字节。
此时计算大小为9,总大小应该符合总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,最大对齐数为4,所以总大小应为12.
内存示意图:
练习二
char c1占一个字节,从结构体的起始位置开始存储。
char c2占一个字节,对齐数为1.
int i 占4个字节,vs环境下他的对齐数为4,所以他在地址中要从4的倍数开始储存。
此时结构体的总大小是8个字节,为最大对齐数4的倍数。
示意图:
显示结果
练习三
练习四
结构体的嵌套问题
大部分的参考资料都是这样说的:
假如在 32 位 的机器上, 一次读取数据只能读取 4个字节,那么在不考虑并对齐的情况下, int类型的数据要读取两次才能完整计算, 而在考虑对齐的情况下,int 只需读取一次。
总的来说:
那在设计结构体的时候,我们既要满足对齐,又要节省空间,怎样才能做到?
让占用空间小的成员尽量集中在一起
s1 和 s2类型的成员一模一样,但是 s1 和 s2 所占空间的大小有了一些区别。
#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数
这里我们将默认对齐数修改为8,所以打印的结果为 12
这里设置默认对齐数为1,就相当于连续存放,结构体的大小为 1+4+1=6
结论:
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。
当我们有一个结构体类型,又创建了一个结构体变量,当我们使用这个变量时,没有直接使用,而是传给其他函数使用,我们可以选择传送这个结构体变量的值,也可以选择传送这个结构体变量的地址
我们向print函数分别传送了struct 结构体本身,以及结构体的地址,那如果比较的话,print1 和 print2 哪个更好?
答案是:首选print2函数。
原因:
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
举一个例子,传结构体本身的话,这个结构体内部有int i[10000]的数据,那我们就要传送10000个整形数据,但是如果我们传送的是结构体的地址的话,首先指针总共就只占用4个字节,我们可以通过这4个字节来找到并操作所指向的10000个整形数据。大大节省了空间和时间。
值传递:函数的形参值传递,是拷贝实参的内容到形参中并且开辟出同等大小的空间来存储的过程,在这个过程中回发生压栈,如果结构体成员过多,压栈的空间就越多,导致空间上的浪费。
址传递:直接把实参的地址传递过去,而使用的空间仅仅是4/8个字节,而且地址传递还能对实参进行修改,所以一般结构体传参都是传址。
结论:
结构体传参的时候,要传结构体的地址。
1.位段的成员必须是 int ,unsigned int ,或 signed int
2.位段的成员名后面有一个冒号和一个数字。
A就是一个位段类型
那位段A的大小是多少?
我们可能会这样计算:2+5+10=17,但是位段的内存分配并不是简单的相加减等等。
这里位段的位是 二进制位,为什么要引入位段的概念呢?我们要知道,如果在内存中要存储 0 、1、2、3这样的数据,所占的空间大小最多才为2个bit位,但是我们创建了一个int类型占4个字节,32个bit位来进行储存这个数据,导致了内存空间的浪费,所以位段可以帮我们在一定程度上节省空间。
好,我们细细分析,首先位段成员必须是 整形家族类型的数据,这是位段的规定。在计算空间大小时,我们应该这样计算:
以上面 struct A 为例,来分析该位段的大小如何计算。
int a:2 // a占2个bit位int b : 5 // b占5个bit位int c : 10 // c占10个bit位int d:30 // d占30个bit位
位段一次开辟4个字节的空间,将a,b,c,d依次放入
但是我们发现将a,b,c已经占了17个bit位了,d 占30个bit位无法存入这个空间中,我们就需要再开辟一个4个字节的空间大小。所以总共占据8个字节进行存储。
但是我们搞清楚了空间的大小,却不清楚数据如何进行存放。d存放的可能方式
1.将第一个开辟的空间剩余的13个bit位占满后,在第二个空间中存放17个bit位。
2.因为无法整体存入第一空间中,所以从第二个空间进行存放,第一个空间剩余的内存被浪费掉。
如何进行存放,我们还不是特别清楚。
所以这里的第三条内存分配的规则就讲述了位段跨平台的特点,其实在不同的编译器上存放的规则也不相同,但是上述2种存放的方法都对,不过在不同的平台存放的形式有所差异。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
3.位段的成员在内存中是从左向右分配,还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳剩余的位时,是舍弃剩余的位还是利用,还是不确定的。
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
这里就可以使用枚举了。
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1。如下图所示:
当然定义的时候也可以赋初值,例如:
为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
联合也是一种特殊的自定义类型。这种类型定义的变量也包含了一系列的成员,特征是这些成员共用一块空间(所以联合也叫共用体)。
比如
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
那么联合体成员变量是如何存放入内存的?
我们看一下代码:
i 和 c 如何进行存储?
来看打印结果
我们会发现i 和 c 占用了同一块空间。
i 和 c 会占用同一块空间,这就是联合体(共用体)的特点。联合体的成员会共用一块空间。
对联合体的特点我们清楚以后,那我们什么时候使用联合体呢?
以上面的 char c 、 int i 举例,在我们只使用char c 或 int i 时我们可以使用联合体。如果有多个成员,使用c的时候,i的值也会发生相应变化,使用i的时候,c的值也会发生相应的变化,所以这两个变量不能同时使用。
举一个例子:
当联合体中存储两个不同的值时,内存中的结果是显示的。
打印的结果是什么呢?
那么 为什么呢?
u=0x55 改变了原来 i 的首字节44的存储,所以共用一块空间的结构体成员在使用时会互相影响。
面试题
判断当前大小计算机的大小端存储
这是普通方法的代码实现:
下面我们用联合体的形式来判断当前机器的大小端存储。
1.联合的大小至少是最大成员的大小。
2.当最大成员不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
比如:
第一个打印结果, char 类型的数组占5个字节,该联合体最大对齐数是4,所以要浪费3个字节的空间,占8个字节。
第二个打印结果, short类型的数组占14个字节,在联合体最大对齐数是4,所以要浪费2个字节的空间,占16个字节。