结构体内存对齐&结构体实现位段

1.结构体内存对齐

        结构体内存对齐其实就是对应着结构体在内存中的存储结构,如何给结构体分配内存,我们先引出一个例子:

        由上图所示,同样的结构体定义类型,唯一的区别就是在结构体中定义的类型顺序不一样,带来的结果就是分配的内存不一样,导致这样的结果就是因为结构体对齐。 

1.1结构体对齐规则

        1.结构体的第一个成员对齐到和结构体变量起始位置偏移量位0的地址处。

        2.其他成员变量要对齐到对齐数的整数倍的地址处。

                对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。

                VS中:对齐数默认值为8。

                Linux和gcc中,没有默认的对齐数,对齐数就是成员自身的大小。

        3.结构体大小为最大对齐数的整数倍(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)。

        4.如果出现嵌套结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍(这个最大对齐数包括嵌套结构体成员的对齐数)。

        通过以下的图解理解以上的结构体对齐规则:

        偏移量:由上红色数字所示,当结构体开始存储的位置,偏移量开始计数,以0起步,之后每往后一个便+1。

        成员变量要对齐到对齐数的整数倍处,(用的VS2022,默认对齐数8)当存第一个C2时,成员变量大小为1字节,默认对齐数为8,取最小值,所以直接存放,对于第二个时,但是在存放整型i 时,成员变量大小为4字节,默认对齐数8,所以得出,对齐数为4,存放在偏移量为4的倍数的位置,所以直接存在偏移量为4的位置,存放C1,对齐数为1,直接存储;

        虽然计算出了C1 C2 i 在内存中存储,但是还没有计算整个结构体在内存中的分配,结构体在内存中的分配还应该遵循为最大对齐数的整数倍,在struct S1中,最大对齐数为4,所以分配的内存一定是4的倍数,上面分配完C1 C2 i 已经是9个字节了,还需要分配更多3个字节,补成4的倍数12。

        当我们计算嵌套结构体的内存大小时,先给出以下例子:

        计算出的嵌套结构体的内存大小为32,先计算S3的内存大小:double的变量大小为8个字节,对齐数为8,第一个变量,偏移量0开始,char的对齐数为1,char位于的位置在偏移量为8的位置,int的对齐数为4,从偏移量12开始,在15结束,一共分配了16个字节,是最大对齐数的整数倍。所以我们可以得出S3的内存大小为16。

        对于S4的内存计算:C1变量对齐数为1,第一个变量在偏移量为1的位置,struct S3的变量大小为16,VS的对齐数默认值为8,所以对齐数为8,struct S3开始的位置为偏移量为8的位置。double的对齐数为8,前面已经分配到了偏移量为24的位置,刚好也是double对齐数的位置,加上double后偏移量达到32的位置,是最大偏移量8的整数倍,所以最后分配的内存为32。 

1.2为什么存在内存对齐

        1.平台原因:

        不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取特定类型的数据,否则抛出硬件异常。

        2.性能原因

        数据结构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问只需要一次。例:假设一个处理器总是在内存中取8个字节,则地址必须是8的倍数。如果我们能保证所有的double类型的数据的地址都对其8的倍数,那么就可以用一个内存操作来读取或者写值。否则,将要执行两次内存访问,因为对象可能倍分放在两个b8字节内存块中。

        即:结构体的内存对齐是拿空间来换取时间的做法。

        所以为了迎合结构体内存对齐,我们要尽可能的在设计时就节省空间:让占用空间小的成员尽量的集中在一起

        如上图的struct S2。

1.3 修改默认对齐值

        在编译器中,我们可以将指令修改编译器的默认对齐数:

        #pragma,可以修改编译器默认的对齐数。

        如上图的操作,我们将默认对齐数设置为1,计算出来的内存大小为每个变量大小的相加。

2.结构体实现位段 

2.1什么是位段

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

        1.位段成员必须是 int、unsigned int、signed int。

        2.位段成员名后面必须有一个冒号和一个数字。

如:

struct A {
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

        上图就是一个位段类型。

        位段的内存大小又是多少呢?

        由上图所得,位段A所占内存大小为8,下图将为这分配的字节做出解释。

        由上图所示,列出来两种存储方式(因为位段的存储方式没有规定,所以还会有其他类型的存储方式),对于第一种存储方式,当第一个int型内存存放不下时,会开辟另一个int型内存,并且在新开辟的内存接着存放。而对于第二种内存存放,因为存放 _d 存放不下,则将整个 _d 都存放在一个新的变量中。

2.2位段的内存分配

        1.位段成员可以是int、unsigned int、signed int 或者是 char等类型。

        2.位段的空间上是按照需要以4个字节(int)或者一个字节(char)的方式来开辟的。

        3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段

现在我们通过一个例子,来给出位段在内存中的具体分配:

        由上图所示,在内存中,我们一共分配了3个字节给位段 w ,而且在内存中存储的值为62 03 04,原理如下:

        按照上图所示,所以我们得出位段 w 在内存中的存放值。 

2.3位段的跨平台问题

        1.int 位段被当成有符号还是无符号的是不确定的。

        2.位段中的最大数目不能确定。(32为机器最大32,16位机器最大16,若在16位机器中写入27,会出现错误)

        3.位段中的成员在内存中从左向右分配,还是从右向左分配未定义。

        4.当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。

        跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在

2.4位段的应用以及注意事项

        以上详细介绍了位段,但是对于位段在实际应用中,因为跨平台的原因,程序猿很少写出位段的类型,但是也确实存在位段的应用,比如IP数据报:

        在IP数据报的首部格式中,对于每一个固定部分的内存分配,都存在明确的规定,这时我们就可以使用位段进行赋值。 

        因为在一个字节内部的bit位是没有地址的,所以不能对位段的成员使用 & 符号,同时也不能用 scanf 进行输入值,只能放在一个变量中,然后赋值给位段的成员:

struct A {
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main() {
	struct W w = { 0 };
	printf("%zd\n", sizeof(w));
	w._a = 10;
	w._b = 12;
	w._c = 3;
	w._d = 4;
	/*printf("%zd\n", sizeof(struct S1));
	printf("%zd\n", sizeof(struct S2));
	printf("%zd\n", sizeof(struct S3));
	printf("%zd\n", sizeof(struct S4));*/
	/*printf("%zd\n", sizeof(struct A));*/
	struct A a = { 0 };  
	scanf("%d", &a._a);  //错误
	int b = 0;
	scanf("%d", &b);     //正确
	a._a = b;
	return 0;
}

         

  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值