VC和gcc下的结构体字节对齐和位域对齐

2023-05-25,,

    之前对结构体的字节对齐一直是一知半解,今天看了百度百科以及大家的博客后,总算是搞清楚了字节对齐到底是怎么一回事,非常感谢大家的分享。对此,将我的所看和所想分享给大家(希望可以帮上那些和我之前一样对结构体字节对齐不是很清晰的小伙伴),还有希望大牛们对我的小见解给予意见与建议。

    首先,先来介绍什么叫字节对齐?百科上是这么讲的:现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问(即不同架构的cpu为了提高访问内存的速度),这就需要各种类型数据按照“一定的规则”在空间上排列,而不是顺序的一个接一个排放,这就是对齐。那么,这种规则是?:一个变量占用 n 个字节,则该变量的其实地址必须能够被 n 整除,即:存放起始地址 % n = 0,对于结构体而言,这个 n 取其成员中的数据类型占空间的值最大的那个。举个例子,比如有些平台访问内存地址都是从偶数地址开始,对于一个int型(假设32位系统),如果从偶数地址开始的地方存放,这样一个周期就可以读出这个int数据,但是如果从奇数地址开始的地址存放,就需要读两个周期,并对两次读出的结果的高低字节进行拼凑才能得到这个int数据,这样明显降低了读取的效率。

    怎样进行字节对齐?:每个成员按其类型的对其参数(通常是这个类型的大小)和制定对其参数(不指定则取默认值)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。即:

        1、数据类型的自身对齐值: char: 1, short: 2, int、long、float: 4, double: 8。(单位为字节)

        2、结构体自身对齐值:其成员中自身对齐值最大的那个值。

    若指定对齐值value: #pragma pack(value) 

    未指定则取默认值。  

        针对数据类型的成员,它仅有一个对齐参数,其本身的长度(是这个对齐参数的1倍)。对于结构体而言,它使用了多种数据类型。意思即就是:每个成员的起始地址 % 自身对齐值 = 0,如果不等于0则先补空字节直至这个表达式成立。即如下规则:

        1、每个成员的起始地址 % 每个成员的自身对齐值(不是结构体的自身对齐值)=0,如果不等于0则先补空字节直至这个表达式成立。

        2、结构体的长度必须为结构体的自身对齐值的整数倍,不够就补空字节。

    不同平台下对齐系数与示例:

        每个特定平台的编译器都有一个默认的对齐系数,gcc中是4,VC中是8.也可以通过预编译命令#pragma pack(value)来指定该系数,经测试gcc中的value值只能是1,2,4.

        举个例子:

        #pragma pack(8)

        struct A{

        char a;

        long b;

        };

        

        struct B{

        char a;

        struct A b;

        long c;

        };

        struct C{

        char a;

        struct A b;

        double c;

        };

        struct D{

        char a;

        struct A b;

        double c;

        int d;

        };

        struct E{

        char a;

        int b;

        struct A c;

        double d;

        };

    在VC中,

    1、对于struct A,char类型自身对齐值为1,long类型自身对齐值为4,结构体的自身对齐值取其成员最大的对齐值,即大小4.那么struct A在内存中的顺序步骤为:

        (1)、char a,地址范围为0x0000~0x0000,起始地址为0x0000,满足0x0000%1 = 0,这个成员字节已经对齐。

        (2)、long b,地址起始位置不能从0x0001开始,因为0x0001%4 !=0,所以先补空字节,直到0x0003结束 ,即补3个空字节,从0x0004开始存放b,其地址范围为0x0004~0x0007.

        (3)、此时成员都存放结束,结构体长度为8,为结构体自身对齐值的2倍。

    struct A中各成员在内存中的位置为:a*** b,sizeof(struct A) = 8.(每个*代表一位,成员各自代表自己所占的位,比如a占一位,b占4位)

    2、对于struct B,里面有个类型为struct A的成员 b自身对齐值为4,对于long 类型,其自身对齐值为4。故struct B的自身对齐值为4.那么struct B在内存中的顺序步骤为:

        (1)、char a,地址范围为0x0000~0x0000,起始地址为0x0000,满足0x0000%1 = 0,这个成员字节已经对齐。

        (2)、struct A b,地址起始位置不能从0x0001开始,因为0x0001%4 != 0,所以先补空字节,直到0x0003结束,即补3个空字节,从0x0004开始存放b,其地址范围为0x0004~0x0011.

        (3)、long c,地址起始位置从0x0012开始,因为0x0012%4 = 0,其地址范围为0x0012~0x0015.

        (4)、此时成员都存放结束,结构体长度为16,为结构体自身对齐值的4倍。

    struct B中各成员在内存中的位置为:a*** b c,sizeof(struct B) = 16.(每个*代表一位,成员各自代表自己所占的位,比如a占一位,b占8位,c占四位)

    3、对于struct C,里面有个类型为struct A的成员b自身对齐值为4,对于double类型,其自身对齐值为8.故struct C的自身对齐值为8.那么struct C在内存中的顺序步骤为:

        (1)、char a,地址范围为0x0000~0x0000,起始地址为0x0000,满足0x0000%1 = 0,这个成员字节已经对齐。

        (2)、struct A b,地址起始位置不能从0x0001开始,因为0x0001%4 !=0,所以先补空字节,直到0x0003结束,即补3个空字节,从0x0004开始存放b,其地址范围为0x0004~0x0011.

        (3)、double c,地址起始位置不能从0x0012开始,因为0x0012%8 != 0,所以先补空字节,直到0x0015结束,即补4个空字节,从0x0016开始存放c,其地址范围为0x0016~0x0023.

        (4)、此时成员都存放结束,结构体长度为24,为结构体自身对齐值的3倍。

    struct C中各成员在内存中的位置为:a*** b**** c,sizeof(struct C)=24。(每个星号代表一位,成员各自代表自己所占的位,比如a占1位,b占8位,c占8位)

    4、对于struct D,前面的三个成员与struct C是一致的。对于第四成员d,因为0x0024%4 == 0,所以可以从0x0024开始存放d,其地址范围为0x0024~0x0027。此时成员都存放结束,结构体长度为28,28不是结构体自身对齐值8的倍数,所以要在后面补四个空格,即在0x0028~0x0031上补四个空格。则结构体长度为32,为结构体自身对齐值的4倍。

    struct D中各成员在内存中的位置为:a*** b**** c d****,sizeof(struct D) = 32。(每个星号代表一位,成员各自代表自己所占的位,比如a占一位,b占8位,c占8位,d占4位)

    5、对于struct E,各成员在内存中的位置为:a*** b c d,sizeof(struct E) = 24。(每个星号代表一位,成员各自代表自己所占的位,比如a占1位,b占4位,c占8位,d占8位)

        通过struct D和struct E可以看出,在成员数量和类型一致的情况下,后者所占的空间少于前者,因为后者的填充的字节要少。如果我们在编程时考虑节约空间的话,应该遵循将变量按照类型大小从小到大声明的原则,这样尽量减少填补空间。另外,可以在填充空字节的地方来插入reserved成员,例如: struct A{char a; char reserved[3]; int b;};

    这样做的目的主要是为了对程序员起一个提示作用,如果不加则编译器会自动补齐。

    在gcc中,

    由于对齐系数最大只能为4,所以上述结构体占内存大小为:8,16,20,24,24。

    举例如下验证gcc下的字节对齐:

    1、默认情况(对齐系数)

        struct st1{

        char ch; //长度1<n,按1对齐,0%1 = 0,起始相对位置=0;存放区间[0]

        int num; //长度4=n,按4对齐,4%4 = 0,起始相对位置=4;存放区间[4,7]

        long lv; //长度4=n,按4对齐,8%4 = 0,起始相对位置=8;存放区间[8,11]

        };

        整个结构体成员对齐后所占的空间为[0,11],占12个字节,接着结构体本身对齐,成员中最长的是4,n也等于4,所以结构体本身按4字节对齐(即对齐系数)。整个结构体的大小=比整个结构体数据成员所占的总空间大或相等且和对齐系数求模结果为0、与之距离最近的数。

        本例中,12%4 = 0,所以结构体st1占12个字节的空间。

    2、#pragma pack(1)(即n=1)

        struct st1{

        char ch;//长度1=n,按1对齐,0%1=0,起始相对位置=0;存放区间[0]

        int num;//长度4>n,按n对齐,1%1=0,起始相对位置=1;存放区间[1,4]

        long lv;//长度4>n,按n对齐, 5%1=0,起始相对位置=5;存放区间[5,8]

        };

        整个结构体成员对齐后所占的区间为[0,8],占9个字节,接着结构体本身对齐,成员中最长的是4,n等于1,所以结构体本身按1对齐(即对齐系数)。

        本例中,9%1 = 0,所以结构体st1占9个字节的空间。

    3、#pragma pack(2)(即n=2)

        struct st1{

        char ch;//长度1<n,按1对齐,0%1=0,起始相对位置=0;存放区间[0]

        int num;//长度4>n,按n对齐,2%2=0,起始相对位置=2;存放区间[2,5]

        long lv;//长度4>n,按n对齐,6%2=0, 起始相对位置=6;存放区间[6,9]

        };

        整个结构体成员对齐后所占的区间为[0,9],占10个字节,接着结构体本身对齐,成员中最长的是4, n等于2,所以结构体本身按2对齐(即对齐系数)。

        本例中,10%2 = 0,所以结构体st1占10个字节的空间。

    为什么说#pragma pack(n)中n只能是1,2,4呢?

    比如3,如果n=3,在编译的时候会警告“对齐边界必须是二的较小次方,而不是3”,也就是说这样设置是没有用的,按默认对齐系数对齐。

    再如8,会有什么结果?看下例:

    4、#pragma pack(8)(即n=8)

        struct st1{

        char  ch;

        long  long num;

        short n;

        int   n;

        };

        假设8起作用,分析如下:

        struct st1{

        char  ch;       //长度1<n,按1对齐,0%1=0,起始相对位置=0;存放区域[0]

        long  long num; //长度8=n, 按8对齐,8%8=0,起始相对位置=8;存放区域[8,15]

        short n;        //长度2<n, 按2对齐,16%2=0,起始相对位置=16;存放区域[16,17]

        int   n;        //长度4<n, 按4对齐,20%4=0,起始相对位置=20;存放区域[20,23]

        };

    整个结构体成员对齐后所占的区间为[0,23],占24个字节,接着结构体本身对齐,成员中最长的是8,n=8,所以结构体本身按8对齐(即对齐系数)。24%8=0,所以占24个字节。

    然而,很不幸,运行的结果是20.接下来用默认的对齐系数4来分析:

        struct st1{

        char  ch;       //长度1<4,按1对齐,0%1=0,起始相对位置=0;存放区域[0]

        long  long num; //长度8>4, 按4对齐,4%4=0,起始相对位置=4;存放区域[4,11]

        short n;        //长度2<4, 按2对齐,12%2=0,起始相对位置=12;存放区域[12,13]

        int   n;        //长度4=4, 按4对齐,16%4=0,起始相对位置=16;存放区域[16,19]

        };

    整个结构体成员对齐后所占的区间为[0,19],占20个字节,接着结构体本身对齐,成员中最长的是8,n等于4所以结构体本身按4对齐(即对齐系数)。20%4=0,所以占20个字节。与运行结果一致。

    

    综上,当n=8的时候gcc仍然使用的是默认的对齐系数4.

    位域

      有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量是,只有0和1两种状态,用一位二进制位即可。为了节省存储空间,并使处理简便,c语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

     位域定义与结构体定义相仿,其形式为:

        struct 位域结构名

        {位于列表};

        其中位于列表的形式为:

            类型说明符 位域名:位域长度

        例如:

        struct bs

        {

        int a:8;

        int b:2;

        int c:6;

        };

        如果结构体中含有位域(bit-filed),那么VC中准则又要有所更改:

        1)如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;

        2)如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;

        3)如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC++6采取不压缩方式(指不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;

    struct s1

    {

    int    i:8;

    char   j:4;

    int    a:20;

    double b;

    };

    struct s2

    {

    int    i:8;

    char   j:4;

    int    a:21;

    double b;

    };

    sizeof(struct s1), VC中为24,gcc中为12

    sizeof(struct s2), VC中为24,gcc中为16

        4)如果位域字段之间穿插着非位域字段,则不进行压缩;

    struct s

    {

    char   c:2;

    double i;

    int    a:4;

    };

    在GCC下占据的空间为16字节,在VC下占据的空间是24个字节。

    5)整个结构体的总大小为最宽基本类型成员大小的整数倍。

    

    注意:

        对齐模数的选择只能是根据基本数据类型,所以对于结构体中嵌套结构体,只能考虑其拆分的基本数据类型。而对于对其准则中的第二条,确实要将整个结构体看成是一个成员,成员大小按照该结构体根据对齐准则判断所得的大小。

        类对象在内存中存放的方式和结构体类似,这里就不再说明。需要指出的是,类对象的大小只是包括类中非静态成员变量所占的空间,如果有虚函数,那么再另外增加一个指针所占的空间即可。

      呼呼~~,终于写完了,非常感谢参考大神的讲解,自己写完之后对这块的知识又熟悉了很多,额。。。。以后还是要多巩固巩固,彻底改变自己以前错误的思想。