0%

C 迷你系列(三)内存对齐

引言

现代计算机一般是 32 比特或 64 比特地址对齐,如果要访问的变量没有对齐,可能会触发【总线错误】。当数据小于计算机的字(word)尺寸,可能把几个数据元素放在一个字中,称为包入(packing)。许多编程语言自动处理数据结构对齐。C 语言或者汇编等语言允许特别控制对齐的方式。【维基百科】

示例

我们以这个例子为说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct p
{
char a;
int b;
short c;
double d;
};
int main()
{
printf("char : %lu\n", sizeof (char));
printf("int : %lu\n", sizeof (int));
printf("short : %lu\n", sizeof (short));
printf("double : %lu\n", sizeof (double));
printf("size of p is %lu\n", sizeof (struct p));
return 0;
}

--- output
char : 1
int : 4
short : 2
double : 8
size of p is 24

问题

从以上例子中我们知道每个类型占用的字节大小,最后得出总的内存大小是 24 字节。如果说总的类型大小是 15 个字节的话,那么多出来的 9 个字节就是填充后的内存大小了,但是这个 24 字节是怎么算出来的呢?

分析

  • 首先 a 是结构体的第一个元素,所以我们假定它的地址为 0x0;a 是 char 类型,也就是占用一个字节。得出 b 的首地址是 0x0 + 1 = 0x1;
  • b 是 int 类型,占用 4 个字节,0x1 不能被 4 整除,所以需要补充额外的 3 个字节,让 b 的地址从 0x4 开始,同时得出 c 的地址 0x8;
  • c 是 short 类型,占用 2 个字节,0x8 的地址可以被 2 整除,所以不需要填充,从而得出 d 的地址为 0x10;
  • d 是 double 类型,占用 8 个字节,但是 0x10 不能被 8 整除,所以需要填充 6 个字节,即:0x16(只是为了说明方便,采用十进制说明);
  • 此时得出总的内存大小为 1 + 4 + 2 + 8 + 3(填充) + 6(填充) = 24 字节,该结构体中最大的内存类型为 double 8 字节,24 字节可以被 8 整除,所以不再需要填充。

内存图



验证

说了这么多,对不对呢?我们来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
struct p val;
printf("address of a is %p\n", &val.a);
printf("address of b is %p\n", &val.b);
printf("address of c is %p\n", &val.c);
printf("address of d is %p\n", &val.d);
return 0;
}
--- output
address of a is 0x7ffeeca67740
address of b is 0x7ffeeca67744
address of c is 0x7ffeeca67748
address of d is 0x7ffeeca67750

比较幸运的是,地址跟我们例子里说明的一样都是 0 为起始地址,从地址上我们验证我们之前的分析是正确的。当然有一点需要注意的是,为了说明方便,这里地址用的是十进制,即 0x16,其实这是错的,因为地址是 16 进制。

补充

我们在分析的最后有提到过总的内存大小与结构体最大成员内存的整除关系,这是什么意思呢?其实就是说整个结构体是否需要内存填充进行对齐。我们看下边一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct p
{
char a;
int b;
short c;
double d;
short e;
};
int main()
{
struct p val;
printf("size of p is %lu\n", sizeof val);
printf("address of a is %p\n", &val.a);
printf("address of b is %p\n", &val.b);
printf("address of c is %p\n", &val.c);
printf("address of d is %p\n", &val.d);
printf("address of e is %p\n", &val.e);

return 0;
}
--- output
size of p is 32
address of a is 0x7ffee624b738
address of b is 0x7ffee624b73c
address of c is 0x7ffee624b740
address of d is 0x7ffee624b748
address of d is 0x7ffee624b750

我们在结构体最后添加一个 short 类型的成员,结果整个内存变成了 32 字节,也就是说增加了 6 字节填充,这是为什么呢?因为增加了 short 之后,整个内存变成了 26 字节,但是 26 字节不能被结构体中最大的 double 8 字节整除,所以还需要在结构体最后补充 6 字节才行。

顺序很重要

从上述例子中,我们发现增加了 short 之后,整个内存变成了 32 字节。那么我们思考一下,结构体中字段不变,我们把它们的顺序变更一下,你认为它们的空间会发生变化吗?

1
2
3
4
5
6
7
8
struct p
{
char a;
int b;
short c;
short e;
double d;
};

如上图,我们把最后的 short 往上移动了一下,让 e 在 d 的上边。猜猜内存如何变化?

1
2
3
4
5
6
size of p is 24
address of a is 0x7ffeebe98740
address of b is 0x7ffeebe98744
address of c is 0x7ffeebe9874a
address of d is 0x7ffeebe98750
address of e is 0x7ffeebe98748

它的内存就又变成了 24 字节,为什么,就是因为 e 提升之后,e 与 d 之间的内存填充发生了变化,如图:



所以,我们发现顺序也会影响内存的大小,这个需要特殊注意。

改变内存填充

那么有没有办法破坏这种填充,让程序员自己决定该如何填充呢?答案是肯定的,C 灵活的地方就在于你可以“任意”改变游戏规则。

  • 法一
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    struct __attribute__((packed)) p
    {
    char a;
    int b;
    short c;
    short e;
    double d;
    };
    int main()
    {
    struct p val;
    printf("size of p is %lu\n", sizeof val);
    printf("address of a is %p\n", &val.a);
    printf("address of b is %p\n", &val.b);
    printf("address of c is %p\n", &val.c);
    printf("address of d is %p\n", &val.d);
    printf("address of e is %p\n", &val.e);

    return 0;
    }
    --- output
    size of p is 17
    address of a is 0x7ffeedbae748
    address of b is 0x7ffeedbae749
    address of c is 0x7ffeedbae74d
    address of d is 0x7ffeedbae751
    address of e is 0x7ffeedbae74f
    总的内存大小就是实际的字段大小了,但是这里取字段地址的时候会提示警告信息:Taking address of packed member 'xx' of class or structure 'p' may result in an unaligned pointer value。
  • 法二
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #pragma pack(1)
    struct p
    {
    char a;
    int b;
    short c;
    short e;
    double d;
    };
    #pragma pack()

    int main()
    {
    struct p val;
    printf("size of p is %lu\n", sizeof val);
    printf("address of a is %p\n", &val.a);
    printf("address of b is %p\n", &val.b);
    printf("address of c is %p\n", &val.c);
    printf("address of d is %p\n", &val.d);
    printf("address of e is %p\n", &val.e);

    return 0;
    }
    --- output
    size of p is 17
    address of a is 0x7ffee3a3c748
    address of b is 0x7ffee3a3c749
    address of c is 0x7ffee3a3c74d
    address of d is 0x7ffee3a3c751
    address of e is 0x7ffee3a3c74f
    #pragma pack(n) 其中 n 代表按照 n 字节进行填充,你可以随意指定,怎么样?是不是很灵活?当然,你要为你自己的程序负责哈!最后,#pragma pack() 用来取消当前设置的对齐方式,也就是说作用域限制在当前结构体。
    最终内存图: