结构体内存对齐

一直以来对结构体内存对齐的知识都是十分模糊的,直至今日下定决心搞清楚它。

结构体

为什么要有结构体内存对齐?

平台原因

不是所有的硬件都能随意的访问任意地址处的任意数据, 有的CPU只能从内存上地址为4的倍数的地址处读取数据,而为了可以读取结构体数据,结构体的起始位置的地址理所应当的为4的倍数,而在结构体内部维护内存对齐是为了迎合硬件的这一特性。

性能原因

因为为了CPU能够快速访问,提高访问效率,变量的起始地址应该具有某些特性,这就是所谓的“对齐”。例如在32为平台上,CPU一个只可以读取出4字节的内容,且只可以在4的整数倍位置进行访问,而当结构体中存放一个int变量时,结构体的起始位置一定时4的倍数,此时若在结构体内部不维护内存对齐,则取出一个整型的4字节,若该int变量的偏移量为3,则就需要先读取偏移量为0向后的4字节,将最后一个字节中属于int成员变量的数据保存下来,再读取偏移量为4向后的4字节,将前这4字节中属于int成员变量的前3字节的数据再次保存,之后将这两段数据进行拼接,才能得到完整的int成员变量。

结构体内存对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处

    • 当程序要存储一个结构体时,会在内存上的某一个地址处开始存储这个结构体,这时不论这个结构体的第一个成员是什么,都要从该地址的0偏移量处开始存储。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的偏移量的地址处。对齐数=编译器的默认的对齐数与该成员大小的较小值

    • 对齐数 = 编译器默认对齐数<成员大小?编译器默认对齐数:成员大小。

      这里使用例子来帮助理解:

      假如第一个成员使用了1个字节的内存,这时按照道理应该就从偏移量为1的地方开始存放第一个成员(假设第二个成员大小为4),但是编译器并不是这样做的,编译器先计算出对于第二个成员变量的对齐数(vs默认对齐数为8,Linux默认对齐数为4)为4,检测到偏移量为1,但1并不是4的整数倍,那么就要继续向后找,知道找到偏移量为4,偏移量为4恰好是4的整数倍,那么就可以从偏移量为4的位置开始存储第二个成员变量了。

      这时后紧接着又要存放第三个成员变量(假设第三个成员变量的大小为1),这时候编译器先要计算出属于第三个成员变量的默认对齐数为1,接下来编译器就来看看下一个位置的偏移量符不符合要求,下一个位置的偏移量为9,9是1的整数倍,所以第三个成员变量就从偏移量为9的位置开始存放了。

    • Linux默认偏移量为4

    • vs默认对齐数为8

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

    • 当编译器将结构体的成员数据都存储完成之后,接下来就会计算处这些成员变量中的最大对齐数(在本例中为4),得知最大对齐数为4,编译器就会查看当前结构体的总大小,发现当先结构体的总大小为9,9并不是4的整数倍,那么编译器就继续占用空间,占到12时就会停止,因为12是4的整数倍。

  4. 如果结构体内部嵌套了别的结构体,嵌套结构体的对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的所有成员的对齐数)的整数倍数

    • 也就是说,当一个结构体内嵌套了另外一个结构体时,被嵌套的结构体成员变量存放完成时要计算最大对齐数只需要从自己的成员变量对齐数中选出最大的即可,但是外部的结构体成员变量存放完成之后计算最大结构体时,要从自己的成员变量的对齐数和嵌套的结构体的成员变量的对齐数中挑选处一个最大的对齐数。

编译器存储一个结构体的过程

当一个结构体要存储在内存中时,编译器会做如下的事情:

  1. 首先一个结构体要存储到内存中时,编译器首先会分配一个地址,作为该结构体的起始位置。
  2. 编译器先拿到结构体的第一个成员变量,直接从相对起始位置0偏移量的位置开始存放第一个成员变量。
  3. 第N(N = 1,2,3 …)成员变量存放完成之后,编译器又拿到第N+1个成员变量,此时编译器就要计算第N+1个成员变量的大小(用来计算该成员变量的对齐数),使用该成员变量大小与编译器的默认对齐数比较,较小的即就是该成员变量的对齐数,拿到对齐数之后,检测当前的偏移量是否为对齐数的整数倍。
    • 如果是,编译器从当前偏移量位置开始存放第N+1个成员变量。
    • 如果不是,编译器向后偏移(占用空间),直到偏移量为该成员变量对齐数的整数倍之后,开始从当前偏移量位置存放第N+1个成员变量。
  4. 当所有的结构体成员存放完成之后,编译器会回想出成员变量中最大的对齐数,之后检测当前结构体大小是否为最大对齐数的整数倍。
    • 如果是,则结构体存放全部完成。
    • 如果不是,编译器继续向后偏移(占用空间),直到当前结构体大小为最大对齐数的整数倍,则结构体存放全部完成。

查看结构体中变量相对起始位置的偏移量

#include<stddef.h>

size_t offsetof(type, member);

//源码
#define offsetof(s, m) (size_t)&(( (s *)0)->m)

参数

  • type:结构体类型
  • member:结构体的成员

返回值

  • 结构体成员相对与结构体起始位置的偏移量

改变结构体的默认对齐数

#pragma pack(N)

使用N来改变编译器的默认对齐数为N。

#pragma pack()

恢复编译器的默认对齐数。

位段

位段是不跨平台的

位段与结构体的区别

  1. 位段的成员必须是int unsigned int signed int
  2. 位段的成员名之后必须要有一个冒号和一个数字。

位段的例子

struct A
{
    int a:2;//a成员只需要2个比特位
    int b:4;//b成员只需要4个比特位
    int c:8;//c成员只需要8个比特位
    int d:20;//d成员需要20个比特位
};

位段存放成员的方式

不同的编译器存放位段的方式是不同的,所以位段是不跨平台的。但主要有以下两种方式,这里使用一个简单的位段来描述这个问题:

struct A
{
    int a:2;
    int b:8;
    int c:10;
    int d:30;
};
  • 编译器先开辟出一个(int)4字节用来存放位段,位段的第一个成员使用2个比特位,则编译器将开辟好的字节的前两个比特位分配给第一个成员,此时还是=剩下30个比特位,第二个成员需要8个比特位,则编译器又分配了8比特位给第二个成员,此时还剩下22个比特位,第三个成员需要10个比特位,则编译器又分配10个比特位给第三个成员,此时开辟好的空间中只剩下12个比特位,但此时第四个成员需要30的比特位,在此处不同的编译器就会有不同的处理方式:
    • 一种编译器会直接浪费掉剩下的12字节,为第四个成员重新开辟一个(int)4字节的内存来存放。
    • 另一种编译器会将第四个成员变量的前12个字节存进剩下的空间中,为剩余的成员重新开辟内存存放。

位段存在的问题

  • 不可移植性,位段在不同的系统中会出现不同的结果。
  • int位段被当作有符号数还是无符号数是不可预计的。
  • 位段中最大位的数目是不确定的。
  • 位段中的成员在内存中从左向右分配,还是从右向左分配是没有经过定义的。

联合体(共用体)

联合体也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间。

联合体的内存分配规则

  1. 联合体分配的大小为成员变量中最大的成员大小。
  2. 联合体的总大小必须时联合体成员中最大对齐数的整数倍。

联合体的特性

判断机器的大小端存储

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值