如何创建和声明一个结构体的内容在操作符详解中已经讲解过了,详情:
C语言·操作符详解https://blog.csdn.net/atlanteep/article/details/134224723
在一般的结构体声明中要包含三个元素 tag(结构体标签) member - list(成员列表) variable - list(变量列表),但是有一种特殊声明,在声明结构体的时候不写结构体标签 tag ,这时它就是个匿名结构体类型,其性质就是只能用一次
就比如这段代码,将x的地址赋给变量p是合法的吗?乍一看,它们的结构体类型都是一样的,所以这么赋值时说的过去的,但是事实上编译器是会报错的
编译器认为了它们的类型是不同的,因为它们都是匿名的,压根就没有类型名,所以根本无法判断它们是不是同一种类型,于是索性就直接认为它们不是同一种类型。
像这种匿名结构体的声明方式,我们只有在确定只会用一次的情况下才会这么声明,一般来说还是要写上结构体类型名(结构体标签)的。
结构体的成员中包含自己本身的用法叫做结构体的自引用,一般结构体的自引用用在数据结构中的链表中,这里我先浅说一下数据结构是什么,之后我会专门开一个系列用来讲解数据结构
数据结构描述的是数据在内存中存储和组织结构,数据结构中包含线性数据结构,树形数据结构,图
顺序表是在一大块内存中开辟一块连续的空间用来存放数据
链表是在一大块内存中随机开辟几个空间用来存放数据,但是这几块空间之间是有联系的,上一块空间中记录着下一块空间的地址,也就是说,找到这块空间我就能知道下一块空间在哪,从而调用下一块空间内的数据。这其中的每一块空间都被叫做一个节点
下面我们尝试定义一个链表的节点
我们看这样一段代码
明明只是两个char类型成员的位置不同而已,为何算出来的结构体大小也不同了,这就涉及到结构体内存对齐的知识
1. 结构体的第一个成员对齐到和结构体起始指针的0偏移处,就是说这个结构体内存块的指针起始位置在哪,其第一个成员的起始位置就存在哪
2. 其他成员要对齐到对齐数的整数倍地址处
3. 结构体总大小是所有成员中的最大对齐数的整数倍
4. 如果结构体中嵌套了结构体,嵌套的结构体对齐数是其自身成员中的最大对齐数,其大小就是自身大小
对齐数:编译器默认的对齐数 与 该成员变量大小的 较小值
vs中默认的对齐数是8
Linux中gcc,没有默认对齐数,对齐数就是成员变量自身的大小
下面我来分析一下刚才的案例
我们直接拿struct s2讲解,首先将每个成员的对齐数分析出来,分别是1,4,1。
第一个成员的位置就是结构体内存块的0偏移处(结构体的起始位置)其大小占1个字节。第二个成员的位置是其对齐数4的整数倍,也就是偏移量为4的位置,其大小占4个字节。第三个成员的位置是其对齐数1的整数倍,下一块没有使用过的内存分配给它,也就是偏移量为8的位置,其大小占1个字节。
此时给结构体开辟了9个字节的空间,并不满足规则3,结构体大小是其最大对齐数的整数倍,所以要将其扩展成符合规则的,最大对齐数是4,所以struct s2的大小要扩展成12个字节
剩下那些空白的内存块就是被浪费掉了,除非这块结构体内存被释放,否则其中并不会再有数据写入。
当然,我从来不骗人,C语言中有一个宏可以验证一下我的话
offsetof (type,member);
使用要包含头文件
第一个参数是结构体类型名,第二个参数是成员名,其返回的结果就是这个成员在结构体中的偏移量,返回类型是size_t
官网资料:offsetof - C++ Reference
这样,我们验证出来了三个成员的偏移量确实分别是0,4,8
第一点是平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定的数据类型,否则抛出硬件异常
第二点是性能原因:数据结构(尤其是栈)应该尽可能的在自然边界上对齐,原因在于对齐了的内存处理器只需要访问一次就可以取到全部数据,而未对齐的内存需要访问两次才能取到全部数据
总的来说,结构体的内存对齐是拿空间换时间的做法
在设计结构体的时候,我们既要考虑对齐又要节省空间的方法就是:让占用空间小的成员尽量集中在一起,就像刚才的结构体s1和s2的例子
#pragma 这个预处理指令是可以修改编译器默认对齐数的,一般我们会将默认对齐数修改成2的几次方的值
我们可以明显的看出来现在结构体s2的3个变量都紧挨在一起了
结构体传参实例
结构体传参的时候最好使用指针的方式传递,因为函数传参的时候,参数需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销将会较大,这会导致性能的下降
位段的声明和结构体类似,知识有两点不同。第一点是位段的成员必须是int、unsigned int、signed int,在C99中位段的成员可以选择其他类型。第二点是成员名后面要跟一个冒号和一个数字。
我们来实现开辟一个位段,并观察一下它于结构体的不同之处
很明显,如果按结构体大小的计算方式看的话,这个结构体A应该占用4*4=16个字节,但是事实上这个位段只占用了8个字节
这就是位段的作用:节省空间
我开辟这个位段意思是给变量_a分配2个比特位,给变量_b分配5个比特位,给变量_c分配10个比特位,给变量_d分配30个比特位,这么一算总共需要47个比特位,相当于一个7个字节多一点,但是结果是开辟了8个字节。
所以说即使是为了节省空间而生的位段,也是会有空间的浪费,只是浪费量大大减少了。这些只是大概简述一下位段是如何分配空间的,接下来我会以vs平台为例讲解vs平台下的位段是怎么分配空间的
1.位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
在vs中运行下面这段代码,观察s变量的内存情况
下面我们分析一下程序的运行过程
位段中变量类型的char、int的意思是当上一块空间不够用之后下一块空间开辟几个字节,比如刚才A的位段就是在4个字节中分配完2、5、10给比特位之后发现分配不下30个比特位了,于是就又开辟了4个字节。
首先s变量的类型是struct S因此计算机首先给s开辟这个类型需要的空间,3个字节。
a需要3个比特位,所以在第一个字节末尾给3个比特位存10,但是10的二进制是1010存不下,于是只能舍弃第一位,只存010。接下来b需要4个比特位存12,于是存在下一组比特位上。接下来为c开辟5个比特位,但是这个字节中已经存不下了,所以就将c的值3存到下一个字节中。最后给d分配4个比特位,第二个字节中也没那么多比特位了,就放到第三个字节当中。
因为s早已经被初始化成0了,所以说s变量维护的那三个字节早就被初始化成0了,因此我画的那些空着的比特位中填的其实都是0
接下来我们按16进制把这个s的内容解析一下,发现和内存中存的值是一样的,这也验证了我们对vs中是如何使用位段的猜测是正确的
最后,有一个有意思的点值得说一下,我们观察内存中的这些值,这里我设置的是4个字节为一行展示内存块的,也就是说62是一个字节,03是一个字节,04是一个字节,cc是一个字节。最后面那个b..?之前讲过,意思是这四个字节可能代表的字符,并没有实际意义。
vs会把那些暂时没用的内存块中填上cc,这也是为什么当字符数组越界打印的时候可能会打印出来一堆 烫烫烫 字样的乱码。通过这一点我们可以分析出s变量确实只操作了3个字节,第四个字节还保持着原始的没有操作痕迹的cc
下图是网络协议中IP数据报的格式,我们可以发现其中很多属性只需要几个比特位就可以描述,这里使用位段,既可以节省使用空间,又有利于网络传输的畅通性
一个字节中的比特位是没有地址的,所以我们不能对位段中的成员变量使用 &符号
就比如试图直接scanf一个位段成员,这种做法是错误的,正确做法是先scanf到一个变量中,再把这个变量赋值给位段的成员