C语言字节对齐

什么是字节对齐

现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。
字节对齐的问题主要就是针对结构体。

对其准则

四个重要的基本概念:

  1. 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。
  2. 结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。
  3. 指定对齐值:#pragma pack (value)时的指定对齐值value。
  4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
    基于上面这些值,就可以方便地讨论具体数据结构的成员和其自身的对齐方式。
    其中,有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N表示“对齐在N上”,即该数据的“存放起始地址%N=0”。而数据结构中的数据变量都是按定义的先后顺序存放。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐存放,结构体本身也要根据自身的有效对齐值圆整(即结构体成员变量占用总长度为结构体有效对齐值的整数倍)。

    三个准则:

  5. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
  6. 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
  7. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
    对于以上规则的说明如下:
    第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址(我们自己计算结构体变量大小时,假想首地址为0即可)。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
    第二条:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
    第三条:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */
    #define OFFSET(st, field) (size_t)&(((st*)0)->field)
    typedef struct{
    char a;
    short b;
    char c;
    int d;
    char e[3];
    }T_Test;

    int main(void){
    printf("Size = %d\n a-%d, b-%d, c-%d, d-%d\n e[0]-%d, e[1]-%d, e[2]-%d\n",
    sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
    OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
    OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
    return 0;
    }
    执行后输出如下:
    1
    2
    3
    Size = 16
    a-0, b-2, c-4, d-8
    e[0]-12, e[1]-13, e[2]-14
    下面来具体分析:
    首先char a占用1个字节,没问题。
    short b本身占用2个字节,根据上面准则2,需要在b和a之间填充1个字节。
    char c占用1个字节,没问题。
    int d本身占用4个字节,根据准则2,需要在d和c之间填充3个字节。
    char e[3];本身占用3个字节,根据原则3,需要在其后补充1个字节。
    因此,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。

    tips

  8. 将自对齐字节大的变量放在前面,可以使结构体变量占用空间尽可能小.
  9. 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;使用伪指令#pragma pack(): 取消自定义字节对齐方式。