结构体的对齐(结构体成员的对齐)

事实上对于结构体成员对齐来说,我们可以不用太过关心,因为默认就是对齐的,
但是我们这里还需要介绍一下结构体对齐,因为我们想借着讲“结构体对齐”来介绍其它相关知识点

为什么要对齐?

对齐可以让硬件更加快速的访问变量,总之对齐有利于提高空间的访问效率,这就跟现实中存放物品时,
按照物体的尺寸来对齐存放,会让物品的管理更加的规范,这样能够有效的提高我们存取物品的效率,
至于为什么在硬件上访问对齐变量空间时效率更高,这个我们不深究,因为这个涉及到底层硬件的内部原理


一、结构体变量内部成员的对齐

1. 结构体大小

结构体的大小不能简单的等于各个成员大小之和,比如下面两个结构体:

struct A
{
    int    a;  // 4
    char   b;  // 1
    short  c;  // 2
};

struct B
{
    char   b;  // 1
    int    a;  // 4
    short  c;  // 2
};

这两个结构体成员唯一的不同就是成员顺序,但是它们各自的大小是不一样的,

printf("%d\n", sizeof(struct A));  //打印结果:8
printf("%d\n", sizeof(struct B));  //打印结果:12

不管是8还是12,都大于成员之和(7字节)

结构体大小为什么不等于成员大小之和,以及为什么结构体成员顺序调整之后,结构体的大小也会发生变化,
当大家理解结构体对齐之后,大家自然就能理解,就算不使用sizeof来测试,我们自己也能够计算出结构体的大小


2. 结构体成员对齐的规则

结构体成员的对齐,是由编译器在编译“结构体”时来实现的

不同的平台可能有所不同,有编译器可能是按照4字节对齐,
目前我使用的gcc编译器默认是按照8字节对齐,这个48为“对齐系数”

按当前对齐系数8,说明具体的对齐规则如下:
(a)每个成员的地址相对于结构体变量第一个字节地址的偏移,必须整除成员的“对齐值”
(b)如果成员大小,<8字节,成员的“对齐值”为自身大小
(c)如果成员大小,>=8字节,成员的“对齐值”为8
(d)整个结构体变量的大小,为结构体成员变量中,最大成员对齐值的整数倍


3. 对齐举例

1)例子1
定义如下这样一个结构体

struct kk
{
    char a;      // 1<8,a的对齐值为1
    short b;     // 2<8,b的对齐值为2
    int c;       // 4<8,c的对齐值为4
    long long d; // 8>=8,d的对齐值为8
}yy;

成员a:是第0个成员,相对于结构体第一个字节地址偏移位00 % 1 等于0,所以a放在第0字节
成员b:相对于结构体第一个字节地址偏移位2时,2 % 2 等于 0,所以b放在第2字节
成员c:相对于结构体第一个字节地址偏移位4时,4 % 2 等于 0,所以c放在第4字节
成员d:相对于结构体第一个字节地址偏移位8时,8 % 2 等于 0,所以d放在第8字节

刚好,从图片可以看出结构体总共占用16字节,也遵循了最大成员对齐值8的倍数

由于对齐的缘故,结构体成员之间并不是紧密相连的,中间可能有空隙(空字节),这个空隙就是由成员对齐导致的
如果你运行的结果与我们对不上,说明你所使用的编译器默认给的“对齐系数”,与我在这边的不相同


2)例子2

struct kk
{
    short b;   // 2<8,b的对齐值为2
    int c;     // 4<8,c的对齐值为4
    char a;    // 1<8,a的对齐值为1
    long long d; // 8>=8,d的对齐值为8
}yy;

结构体成员还是那些,但是被调整了顺序,按照之前的规则,yy变量的内存结构图为:

虽然从图中看到的yy结构体变量只占用了20个字节空间,但是yy的大小必须为最大对齐值8的整数倍,
所以,yy结构体的空间真正的大小是24字节


二、手动成员对齐之 #pragma

结构体变量默认就是对齐的,我们所用的gcc编译器的“对齐系数”为8
事实上我们完全可以自己指定这个对齐系数,自己指定时就是“手动对齐”


在第二章中,我们一早就介绍过#pragma这个预编译指令,它有很多功能,目前要使用的是它的手动对齐的功能

1. 使用格式

//n为我们手动指定的对齐系数,n可以指定的值一般为1/2/4/8/16,再大就没有意义了,
// 因为C语言基本数据类型最大一般是不会超过16字节的
#pragma pack(n)
...

struct ***  //按照n(对齐系数)来对齐
{
    ...
}

...
#pragma pack() //还原为之前的对齐方式

#pragma后面可以跟很多不同的东西,跟的东西不同,所要实现的通过功能也是不同,
我们这里跟的是pack(),表示要实现的是手动对齐功能

使用#pragma pack(n)手动对齐时,对齐规则不变,与前面的自动对齐是一样的
而且#pragma pack(n)C标准语法特性,所以所有的编译器都支持

2. 例子

/*  默认按照8字节对齐,大小为16 */
struct kk1
{
    char a;     //1<8,对齐值为1
    short b;    //2<8,对齐值为2
    int c;      //4<8,对齐值为4
    long long d; //8>=8,对齐值为8
}yy1;


/* 按照1字节对齐,大小为15
 * 按照1自己对齐后,成员之间时紧密排列在一起的。
*/
#pragma pack(1) //对齐系数为1
struct kk2
{
    char a;   //1>=1,对齐值为1
    short b;  //2>=1,对齐值为1
    int c;    //4>=1,对齐值为1
    long long d; //8>=1,对齐值为1
}yy2;     //1>=1,对齐值为1
#pragma pack()


/* 按照4字节对齐,大小也为16 */
#pragma pack(4)
struct kk3
{
    char a;     //1<4,对齐值为1
    short b;    //2<4,对齐值为2
    int c;      //4>=4,对齐值为4
    long long d; //8>=4,对齐值为4
}yy3;
#pragma pack()


void main(void)
{
    printf("%d\n", sizeof(struct kk1));
    printf("%d\n", sizeof(struct kk2));
    printf("%d\n", sizeof(struct kk3));

    return 0;
}

打印结果:

16
15
16

三、手动成员对齐之 attribute、aligned

使用GCC的对齐指令__attribute__((aligned(n)))来对齐
__attribute__aligned(n)是独立的关键字,而且是GCC编译器支持的特有关键字


__attribute__

实际上我们在第二章就讲过__attribute__,该关键字用于设置属性,比如设置函数的属性、变量的属性、类型的属性等

设置属性的格式为__attribute__(属性),设置属性的目的是为了控制编译器对被修饰代码的优化,
通过__attribute__设置变量的对齐,其实就是在设置变量的属性

__attribute__关键字的用法很多,而且好些都是冷门用法,而且只有在Linux下才能见到,
因此我们没有必要去刨根问底,我们对待__attribute__的原则就是,知道它大概的作用,
以后遇到没见过的用法时,我们再有针对性了解,这样才是最合理

aligned(n)

指定按照按照n字节对齐,n为对齐系数,与pack(n)是类似的


为什么要讲GCC对齐命令?
我们这之所有要讲__attribute__((aligned(n))),是因为在进行Linux嵌入式开发时,
Linux源码中可能会见到,所以我们需要了解


1. 与上一节手动对齐区别

n的取值不同

  • #pragma pack(n)n的值为1/2/4/8/16,再大没有意义
  • __attribute__((aligned(n)))n必须为2的幂次,如果不为2的幂次,编译会报错,
    而且,n可以指定的很大,指定为一个很大数是有意义

规则有所不同:

  • #pragma pack(n):结构体的大小为结构体成员中最大对齐值的整数倍
  • __attribute__((aligned(n))):结构体的大小为n的整数倍

2. 例子

struct kk2
{
    char a;   //1<32,a的对齐值为1
    short b;  //2<32,b的对齐值为2
    int c;    //4<32,b的对齐值为4
    long long d; //8<32,d的对齐值为8
}__attribute__((aligned(32))) yy2;    //yy2的大小为32的整数倍。


void main(void)
{
    printf("sizeof(yy2) = %d\n", sizeof(yy2));
    printf("%d\n", sizeof(yy2) % 32); //打印结果为0,说明整个结构体变量的大小为32的整数倍

    return 0;
}

打印结果:32,刚好能够整除32
为了实现是32的整数倍,最后必须补足空间,让大小为32的整数倍


3. 取消对齐

__attribute__((packed))用于取消对齐,所谓取消对齐其实就是按照1字节对齐,
等价于__attribute__((aligned(1)))

虽然__attribute__((aligned(1)))#pragma pack(1)都是按照1字节对齐的,但是二者有区别

(a)#pragma pack(1)

按照1字节对齐,成员之间是紧密排列在一起的,成员间没有空隙,而且整个大小就是各个成员之和

(b)__attribute__((aligned(1)))

按照1字节对齐后,成员之间也是紧密排列在一起的,成员间也没有空隙,但是在最后面可能会补字节空间,
至于是补多少个字节,由编译器根据实际情况而定,并没有具体的规则

所以使用__attribute__((aligned(1)))或者__attribute__((packed))1字节对齐的话,
由于会补充字节缘故,整个大小并不会刚好等于各个成员之和,而是>=各个成员之和,
到底是>还是=,这个就要看编译器怎么解释

例子

//按照1字节对齐,大小刚好为各个成员之和,为15
#pragma pack(1)  
struct kk1
{       
    char a;     //1>=1,对齐值为1
    short b;    //2>=1,对齐值为1
    int c;      //4>=1,对齐值为1
    long long d; //8>=1,对齐值为1
}yy2;     
#pragma pack()


//按照1字节对齐,但是最后补了1个字节,所以大小位16
struct kk2
{       
    char a;     //1>=1,对齐值为1
    short b;    //2>=1,对齐值为1
    int c;      //4>=1,对齐值为1
    long long d; //8>=1,对齐值为1
}__attribute__((aligned(1))) yy2;  //等价于__attribute__((packed))


void main(void)
{
    printf("%d\n", sizeof(struct kk1));
    printf("%d\n", sizeof(struct kk2));

    return 0;
}

打印结果:

15 
16

四、手动对齐的意义(作用)

本来想介绍一下手动对齐的作用的,但是在没有具体应用背景的情况下,介绍手动对齐作用的例子有些不好理解,
所以现在大家只需要知道如下几点即可:

  • (1)知道结构体默认就是对齐的
  • (2)理解对齐的规则(不需要记忆,理解即可)
  • (3)知道什么是手动对齐,当以后涉及到手动时,基于现在所学的基础知识,大家能够自己去理解搞定

不过有一点可以向大家肯定,在应用开发中,手动对齐用的非常少,只有在偏底层开发中会稍多些。