探索C语言:结构体内存对齐规则与段位类型

前言

我们知道,整型变量有自己的大小,浮点型变量有自己的大小,数组也有自己的大小,只要数据存放到内存中,就会占用内存大小。
所以作为C语言数据类型的一种——结构体 同样也有自己的大小。
要注意的是,结构体虽是多种数据类型的集合,但结构体的大小并不像我们想的那样简单地将每个结构体成员的大小相加就能得到的

想要计算结构体的大小,需要先去了解计算结构体的规则!


一、为什么存在结构体内存对齐?

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
    定类型的数据,否则抛出硬件异常。比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。

  • 性能原因(主要原因):数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    假设一个处理器总是从内存中取4个字节,则:
    在这里插入图片描述

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


二、结构体对齐规则

结构体的大小计算要遵循结构体的对齐规则

  1. 结构体的第一个成员永远都放在0偏移处。(即结构体的首地址处,即对齐到0处)
  2. 从第二个成员开始,以后的每个成员变量都要对齐到某个对齐数的整数倍的地址处。
  3. 当成员变量全部放进去后,结构体的总大小必须是,所有成员的对齐数中最大对齐数的整数倍,如果不够,则浪费空间对齐。
  4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

对齐数 = 该结构体成员变量自身的大小 与 编译器默认的一个对齐数的 较小值
注:VS中的默认对齐数为8。gcc环境下,没有默认对齐数。不是所有编译器都有默认对齐数,当编译器没有默认对齐数的时候,成员变量的大小就是该成员的对齐数。


三、结构体大小计算(三板斧解决!)

举例

struct S
{
	double a;
	char b;
	int c;
};

一板斧

找出每个成员变量的大小将其与编译器的默认对齐数相比较,取其较小值为该成员变量的对齐数。
注:VS2022默认对齐数为8。

在这里插入图片描述

二板斧

根据每个成员对应的对齐数找出它们在内存中的相对位置

在这里插入图片描述

三板斧

通过最大对齐数确定最终该结构体的大小

在这里插入图片描述

通过上面的分析图我们可以知道:

  • 紫部分(double a成员占用)+ 红色部分(char b成员占用)+ 黄部分(int ic成员占用)+红色与黄色之间的白色部分(浪费掉了)总共占用了16个字节的内存空间。
  • 再将总共占用的内存空间(16字节)与结构体成员的最大对齐数(8字节)相比较,此时16正好是8的整数倍,所以该结构体在VS编译器下的大小就16个字节。
  • 注意:如果成员变量占用的总字节个数不是其成员变量中的最大对齐数的整数倍,这时我们需要将其扩大到最大对齐数的整数倍。

如果在设计结构体的时候,我们既想要满足对齐,又想要节省空间,则应该:让占用空间小的成员尽量集中在一起。


四、修改默认对齐数

当结构体的对齐方式不合适的时候,我们可以自己更改默认对齐数,要修改编译器的默认对齐数,需要借助于以下预处理命令:

#pragma pack()

如果在该预处理命令的括号内填上数字,那么默认对齐数将会被改为对应数字。
如果只使用该预处理命令,不在括号内填写数字,那么会恢复为编译器默认的对齐数。

#include <stdio.h>
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char a;
	int b;
	char c;
};
#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char a;
	int b;
	char c;
};
int main()
{
	printf("%d\n", sizeof(struct S1));//结果为12
	printf("%d\n", sizeof(struct S2));//结果为6
	return 0;
}

五、结构体的位段

1、什么是结构体的位段

1、位段成员必须是int、unsigned int或signed int,在C99中其成员可以选其他类型。
2、段位的空间是按照需要以4个字节(int)或者1字节(char)的方式开辟的。
3、位段成员后面有一个冒号和数字。

例如:

struct A
{
   int _a:2;
   int _b:5;
};

其中A就是一个位段类型。

解释:如果给每个int开辟4个字节即32个字节空间,但是比如成员中放入3,二进制为11,它仅仅占据了两个二进制位,结果会导致30个字节的空间形成了浪费,因此我们用位段来限制内存。int _a:2、int _b:5:的意思是a所储数据只占两个字节,b只占5个字节空间。总的来说位段是为了减少空间的浪费。

2、位段的跨平台问题

1、int位段被当成符号数还是无符号数是不确定。
2、位段中最大位的数目不确定(超出机器最大数时会出现问题)
3、位段中的成员在内存中从右向左分配,还是从左向右分配标准未定义。(vs中是从右向左)
4、当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时;是舍弃剩余的位还是利用,这是不确定。

总结:和结构体相比,位段可以达到同样的效果。相比之下的优点是:可以很好的节省空间;缺点是位段有跨平台的问题存在。

3、位段使用的注意事项

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的,所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

4、段位的应用(了解)

段位主要应用于网络协议中,在IP数据报表的格式中,可以看到其中很多的属性只需要几个bit位就能描述,这里使用段位,能够实现想要的效果,也能节约空间,对于网络畅通有帮助。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大白菜不空心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值