【C语言进阶】结构体内存对齐与位段

        在[C语言入门]的板块,我们浅显地讨论过结构体的内容,这一期将深刻介绍结构体,后面的一期内容会做联合、枚举相关的内容。

目录

1.结构体类型的声明

2.结构体的自引用

3.结构体变量的定义和初始化

4.结构体内存对齐(重点)

  4.1内存对齐的规则

练习: 计算s3的大小

练习: 计算s4的大小

 4.2 内存对齐的原因

4.3 内存对齐的缺点

4.4 修改VS默认对齐数

5.结构体传参

6.结构体实现位段(位段的填充&可移植性)

1.结构体类型的声明

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

关于结构体类型的定义,举一个简单的例子:写一个关于学生的结构体,成员列表含姓名、年龄。

        有读者会问了,结构体中的成员变量不需要初始化吗?我们可以把结构体当做一个普通的类型,例如int、char类型,类型是修饰变量的,所以不需要初始化;  

        上图中的s1,s2是根据这个类型创造出的结构体变量,可以不加,此时这个两个结构体变量是全局变量,当然可以创建局部的结构体变量,如下图所示:

         创建结构体变量的时候需要书写struct XXX 结构体变量名,过于繁琐,我们可以使用以下这种方式:

匿名结构体,不能手动创建结构体变量只能使用一次。

        匿名结构体是有一个缺点的,那就是没有类型,如果两个匿名结构体的内部成员变量,我们依然认为这是两个不同类型的结构体。 

能不能对匿名结构体进行使用typedef呢?是不行的,匿名结构体本身没有类型,所以不能进行简化。

 我们还可以这么写,相当于创建了一个结构体指针。

2.结构体的自引用

        我们知道在数据结构中有顺序表和链表。顺序表例如数组,可以理解成在内存中连续存储的数据;

        链表在内存中是分散存储的,节点和节点之间是靠存储下一个节点的信息进行关联的,存储的节点类型和本类型完全一致,这就叫自引用。

         这么写是有问题的,试想一下:next是你下一个节点的内容,next也存放着关于它下一个结构体的内容,如此以来只要链表一长,在最前面的结构体存的内容是海量的,所以为了解决这个问题,我们只需要存储下一个节点的地址即可,每一个节点只能获取下一个节点的相关信息,我们可以用结构体指针来指向下一个结构体;这就是我们常说的数据域+指针域。

3.结构体变量的定义和初始化

struct Point
{
    int x;
    int y;
}p1; // 结构体变量

struct Point p2;// 定义结构体变量p2

// 初始化,定义变量的同时赋初值
struct Point p3 = {x,y};

struct Stu
{
    char name[15];
    int age; 
};
struct Stu s = {"zhangsan",20};// 初始化

我们可以把结构体(类型)当做图纸,结构体变量当做具体的房子。

        结构体中含结构体,进行初始化的时候,只需要加一个大括号即可~

我们可以使用结构体变量+.的形式访问结构体变量的成员变量。

4.结构体内存对齐(重点)

这个问题的本质就是计算结构体的大小。分析一下下图结构体所占空间的大小,似乎6个字节足矣;

 结果是12个字节,思考一下为什么会这样?

        我们再进行一次尝试,这一次S2的大小是8个字节 ,我们惊奇的发现,即使成员相同,成员之间的顺序不同,那么结构体所占的内存空间仍然发生了变化。

  4.1内存对齐的规则

        ①假设结构体变量s1在内存中从下图的位置开始,那么第一个字节相对于内存的其起始位置偏移量是0,以此类推......所以c1占的就是第一个字节;

        ②其他的成员对齐到对齐数的整数倍的地址处;默认的对齐数是由编译器和其他成员字节数的较小值,VS的对齐数是8;s1中的i大小是4个字节,大于默认对齐数8个字节,所以对齐数是4;所以i要存到偏移量为4的地址处(4是4的倍数,如下图所示);中间空的3个字节是没有使用的。

③其他成员都要对齐到对齐数的整数倍数,第三个成员是一个字节,小于8个字节,所以对齐数是1,由于8是1的倍数,所以可以直接往下存。

④此时所有的成员变量存储完毕之后只占用了9个字节,还是和之前预期的12个字节有所偏差。

⑤最后一个规则解释了原因,结构体的大小是最大对齐数的整数倍,那么最大对齐数是4,现在占用的字节数是9,所以想要是4的整数倍,只能是12个字节;所以一共浪费了6个字节。

⑥验证一下分析的过程,使用offsetof这样一个宏定义,可以返回结构体的偏移量。

输出结果与我们分析的一致。 

同理来分析一下S2的内存分布:

①首先c1占用一个字节,内存偏移是0;

②c2的对齐数是1,所以可以继续往下存内存偏移是1,占用一个字节;

③最后i的对齐数是4,所以偏移必须是4的整数,偏移量就是4,中间空了2个字节,i一共四个字节占用到偏移量是8的位置。

④目前占用的字节数是8,最大的对齐数是4,所以整个结构体内存占用必须是4的倍数,所以刚好是8;

练习: 计算s3的大小

 ①double占8个字节,起始位置偏移量是0,一直占用到偏移量为7的地址;

②char的对齐数是1,可以直接放在偏移量为8的位置;

③int对齐数是4,必须放在4的倍数的偏移量地址,只能是12,跳过了3个字节,占用了四个字节,最后一共占用了16个字节。

④最大对齐数是8,16刚好是8的倍数,那么S3占用16个字节。

练习: 计算s4的大小

①char占用0偏移量的地址;

②s3占用的空间是16个字节,这是一个结构体,关于结构体嵌套,使用第四条规则,使用s3的的最大对齐数的整数倍8,直接从8向后存放16个字节,到偏移量23,空了7个字节;

③double d的对齐数是8,24刚好是8的倍数,所以存在24往后顺延8个字节那就是32,一共占用了32个字节;

④32是最大对齐数8的倍数,所以合法,最后占用32个字节。

 4.2 内存对齐的原因

①平台原因:不是所有的硬件平台都可以访问任意地址,某些硬件平台只能在某些地址取出数据,否则抛出硬件异常。

②性能原因:数据结构(尤其是栈)经可能地在自然边界对齐;原因是是为了访问没有对齐的内存,处理器需要两次访问,对齐的内存只需要一次访问。

举一个例子:

         下图左边是没有对齐的内存存储方式,如果32位机器一次访问4个字节,那么访问c之后需要两次才能读取到i完整的数据;

        下图右边是对齐内存的存储方式,读完c之后可以直接读取完整的i。

4.3 内存对齐的缺点

        经过之前的学习,我们可以知道内存对齐的缺点就是浪费空间,但是换来了效率,这既是所谓的空间换时间。 

         我们通过观察s1和s2,两个结构体只是交换了成员变量,中间相差了很多字节,诀窍就是先把占用小的成员放在一起,我们可以一定程度上减少空间的浪费。

4.4 修改VS默认对齐数

我们可以轻易的知道s5占用16个字节,修改默认对齐数为4之后占用字节为12;

其实就是更改了8的对齐数为4;

①i:占用0~3;

②d:对齐数为4,可以从4~11;

③一共12个字节。

如果不想对齐的话,可以使用以下代码:设置默认对齐数为1;

5.结构体传参

        想要打印一个结构体变量的所有内部成员,需要怎么做。

①直接传入结构体。

②传入结构体指针

        这两种形式哪一种更好呢?传值调用还是传址调用 ?函数在传入参数的时候,参数会压入栈空间,这就意味着参数越大占用的栈空间也就越多,所以最好是传入一个指针;

6.结构体实现位段(位段的填充&可移植性)

位段是由结构体实现的,位段的声明和结构体是类似的,有两个不同:

        ①位段的成员必须是int、unsigned int或者signed int。

        ②位段的成员名后面后面有冒号和一个数字。

例如:

         位段的位是比特位的意思,上图的意思是_a需要两个比特位,_b只需要5个比特位以此类推......

        在实际开发过程中可能存在一个判断真假的成员变量取值只有0或1,此时使用位段就会非常节省空间。

        上面的位段占用多少个字节?仔细分析一下有47个bit,那么只需要6个字节就能存下,但是事实上需要8个字节,这是为什么呢??

6.1 位段的内存分配

        位段的内存分配是一次开辟一个或者4个字节,根据成员的类型进行分配,如果成员是int一次就开辟4个字节,如果成员是char,一次开辟1个字节;不够在进行开辟。至此上面的问题得到解决了。

①先开辟4个字节也就是32位,abc用完之后还剩了17bit位,不够用。

②所以又开辟4个字节,一共8个字节。

③d是存在之前没用用完的bit位呢还是存在新开辟的内存中呢?

举一个例子说明:

①给a一个字节8bit,用了3个还剩五个。

②b正好用四个,还剩1个bit。

③c需要用5个,而剩下1个,所以需要重新开辟一个字节,之前的一个bit浪费了,这个字节用了5个还剩3个。

④d需要用4个,还剩三个,于是有开辟一个字节,之前3个bit浪费了。

总结:如果成员不够存,需要重新开辟空间,那么本成员直接存入新开辟的空间。

这个代码还存在一个问题,a只有3个bit位,10的二进制是1010,这里需要进行截断,存入010:

每次申请一个字节,需要从低位到高位使用。

验证:

6.2 位段的跨平台问题

①int位段被当成有符号或无符号是不确定的。

②位段中最大位的数目不能确定。(如果16位机器最大16,如果写成27在16位机器上会出现问题)。

③位段成员在内存中从左向右还是从右向左的分配标准没有定义。

④当一个结构包含两个位段,第二个位段的成员比较大,无法容纳第一个位段的剩余位的时候,是舍弃剩余的位还是使用者是不确定的。

6.3 位段的应用

ip数据报的底层源码就是使用位段实现的。

字段名称长度(位)含义说明
版本(Version)4指示 IP 协议版本,IPv4 为 4。
首部长度(IHL)4单位为 32 位字(4 字节),表示 IP 首部总长度(包括固定部分和可选部分),最小值 5(20 字节),最大值 15(60 字节)。
服务类型(TOS)8早期用于指定服务质量,现通常称为区分服务(DS):前 6 位为 DSCP(区分服务代码点),后 2 位为 ECN(显式拥塞通知)。
总长度16整个 IP 数据报的总长度(字节),最大值为 65535 字节(受限于 16 位)。
标识(Identifier)16用于分片重组,同一数据报的所有分片具有相同标识。
标志(Flags)3分片控制标志:
- 最高位(保留位):必须为 0;
- DF(Don't Fragment):1 表示不分片;
- MF(More Fragments):1 表示还有后续分片。
片偏移13表示分片在原数据报中的相对位置,单位为 8 字节(因此分片大小必须是 8 的倍数)。
生存时间(TTL)8限制数据报在网络中的存活时间,每经过一个路由器减 1,值为 0 时丢弃,防止循环。
协议8指示上层协议类型,如 TCP(6)、UDP(17)、ICMP(1)等。
首部校验和16仅校验 IP 首部,用于检测首部在传输中的错误(数据部分由上层协议校验)。
源 IP 地址32发送方的 32 位 IP 地址。
目的 IP 地址32接收方的 32 位 IP 地址。
可选字段可变(0-40 字节)用于特殊功能(如源路由、记录路由等),长度可变,最长 40 字节(需为 32 位的整数倍)。
填充可变确保首部长度为 32 位的整数倍,用 0 填充。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值