C语言·自定义类型:结构体

1. 结构体类型的声明

        如何创建和声明一个结构体的内容在操作符详解中已经讲解过了,详情:

C语言·操作符详解icon-default.png?t=N7T8https://blog.csdn.net/atlanteep/article/details/134224723

1.1 结构体的特殊声明       

         在一般的结构体声明中要包含三个元素  tag(结构体标签)  member - list(成员列表)  variable - list(变量列表),但是有一种特殊声明,在声明结构体的时候不写结构体标签 tag ,这时它就是个匿名结构体类型,其性质就是只能用一次

                                        

        就比如这段代码,将x的地址赋给变量p是合法的吗?乍一看,它们的结构体类型都是一样的,所以这么赋值时说的过去的,但是事实上编译器是会报错的

    

        编译器认为了它们的类型是不同的,因为它们都是匿名的,压根就没有类型名,所以根本无法判断它们是不是同一种类型,于是索性就直接认为它们不是同一种类型。

        像这种匿名结构体的声明方式,我们只有在确定只会用一次的情况下才会这么声明,一般来说还是要写上结构体类型名(结构体标签)的。

1.2 结构体的自引用

        结构体的成员中包含自己本身的用法叫做结构体的自引用,一般结构体的自引用用在数据结构中的链表中,这里我先浅说一下数据结构是什么,之后我会专门开一个系列用来讲解数据结构

        数据结构描述的是数据在内存中存储和组织结构,数据结构中包含线性数据结构,树形数据结构,图

        顺序表是在一大块内存中开辟一块连续的空间用来存放数据

        链表是在一大块内存中随机开辟几个空间用来存放数据,但是这几块空间之间是有联系的,上一块空间中记录着下一块空间的地址,也就是说,找到这块空间我就能知道下一块空间在哪,从而调用下一块空间内的数据。这其中的每一块空间都被叫做一个节点

                                ​​​​​​​  

        下面我们尝试定义一个链表的节点

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​

2. 结构体内存对齐

        我们看这样一段代码

        明明只是两个char类型成员的位置不同而已,为何算出来的结构体大小也不同了,这就涉及到结构体内存对齐的知识

2.1 对齐规则

        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);

        使用要包含头文件 <stddef.h>

        第一个参数是结构体类型名,第二个参数是成员名,其返回的结果就是这个成员在结构体中的偏移量,返回类型是size_t

        官网资料:offsetof - C++ Reference

        这样,我们验证出来了三个成员的偏移量确实分别是0,4,8

2.2 为什么存在内存对齐

        第一点是平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定的数据类型,否则抛出硬件异常

        第二点是性能原因:数据结构(尤其是栈)应该尽可能的在自然边界上对齐,原因在于对齐了的内存处理器只需要访问一次就可以取到全部数据,而未对齐的内存需要访问两次才能取到全部数据

        总的来说,结构体的内存对齐是拿空间换时间的做法

        在设计结构体的时候,我们既要考虑对齐又要节省空间的方法就是:让占用空间小的成员尽量集中在一起,就像刚才的结构体s1和s2的例子

2.3 修改默认对齐

        #pragma 这个预处理指令是可以修改编译器默认对齐数的,一般我们会将默认对齐数修改成2的几次方的值

        我们可以明显的看出来现在结构体s2的3个变量都紧挨在一起了

3. 结构体传参

        结构体传参实例

        ​​​​​​​        ​​​​​​​                 ​​​​​​​

        结构体传参的时候最好使用指针的方式传递,因为函数传参的时候,参数需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销将会较大,这会导致性能的下降

4. 结构体实现位段

4.1 什么是位段

        位段的声明和结构体类似,知识有两点不同。第一点是位段的成员必须是int、unsigned int、signed int,在C99中位段的成员可以选择其他类型。第二点是成员名后面要跟一个冒号和一个数字。

        我们来实现开辟一个位段,并观察一下它于结构体的不同之处

        ​​​​​​​            

        很明显,如果按结构体大小的计算方式看的话,这个结构体A应该占用4*4=16个字节,但是事实上这个位段只占用了8个字节

        这就是位段的作用:节省空间

        我开辟这个位段意思是给变量_a分配2个比特位,给变量_b分配5个比特位,给变量_c分配10个比特位,给变量_d分配30个比特位,这么一算总共需要47个比特位,相当于一个7个字节多一点,但是结果是开辟了8个字节。

        所以说即使是为了节省空间而生的位段,也是会有空间的浪费,只是浪费量大大减少了。这些只是大概简述一下位段是如何分配空间的,接下来我会以vs平台为例讲解vs平台下的位段是怎么分配空间的

4.2 位段的内存分配

        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

        

4.4 位段的应用

        下图是网络协议中IP数据报的格式,我们可以发现其中很多属性只需要几个比特位就可以描述,这里使用位段,既可以节省使用空间,又有利于网络传输的畅通性

4.5 位段使用的注意事项

        一个字节中的比特位是没有地址的,所以我们不能对位段中的成员变量使用 &符号 

        就比如试图直接scanf一个位段成员,这种做法是错误的,正确做法是先scanf到一个变量中,再把这个变量赋值给位段的成员

  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值