文章

字节对齐

字节对齐

本文介绍 C/C++ 结构体字节对齐规则和处理。

字节对齐

1 字节对齐概念

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

变量存的起始地址必须具备某些特性——“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。

对齐跟数据在内存中的位置有关。为了使得CPU能快速对变量进行访问,变量存的起始地址必须具备某些特性,即“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。

2 字节对齐理由

2.1 不同硬件平台对存储空间的处理上存在不同

某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。

2.2 根本原因在于CPU访问数据的效率问题

以32位机为例,它每次取32个位,也就是4个字节。以int型数据为例,如果它在内存中存放的位置按4字节对齐,也就是说1个int的数据全部落在计算机一次取数的区间内,那么只需要取一次就可以了。如果不对齐,很不巧,这个int数据刚好跨越了取数的边界,这样就需要取两次才能把这个int的数据全部取到,这样效率也就降低了。

image-20220612132733086

2.3 节约空间

应该辩证地看:合理对齐,则节约空间;否则浪费空间。

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
#include <iostream>

typedef struct Test1
{
    int   pub_int;
    short pub_short;
    char  pub_char;
} Test1;

typedef struct Test2
{
    char  pub_char;
    int   pub_int;
    short pub_short;
} Test2;

typedef struct Test3
{
    char  pub_char;
    short pub_short;
    int   pub_int;
} Test3;

int main()
{
    // #define offsetof(struct, field)     (size_t)&(((struct*)0)->field)
    printf("The Sturct Test1 Size: %2ld, Offset: %2ld, %2ld, %2ld\n", sizeof(Test1), offsetof(Test1, pub_int), offsetof(Test1, pub_short), offsetof(Test1, pub_char));
    printf("The Sturct Test2 Size: %2ld, Offset: %2ld, %2ld, %2ld\n", sizeof(Test2), offsetof(Test2, pub_char), offsetof(Test2, pub_int), offsetof(Test2, pub_short));
    printf("The Sturct Test3 Size: %2ld, Offset: %2ld, %2ld, %2ld\n", sizeof(Test3), offsetof(Test3, pub_char), offsetof(Test3, pub_short), offsetof(Test3, pub_int));
}

image-20220612134550749

结构体Test1中包含一个4字节的int数据,一个1字节char数据和一个2字节short数据;Test2、Test3也一样。按理说Test1、Test2、Test3的大小应该都是7字节。之所以出现上述结果,就是因为编译器要对数据成员在空间上进行对齐。

3 字节对齐标准

3.1 对齐准则

  • 结构体变量的首地址能够被其对齐字节数大小所整除
  • 结构体每个成员相对结构体首地址的偏移都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足。
  • 结构体的总大小为结构体对齐字节数大小的整数倍,如不满足,最后填充字节以满足。

64 位机器变量字节

类型字节范围
char1-128~127
unsigned char10~256
(signed) short2-32768~32767
unsigned short20~65536
(signed) int4-2147483648~2147483647
unsigned int40~4294967295
(signed) long8-9223372036854775808~9223372036854775807
unsigned long80~18446744073709551615
(signed) long long8-9223372036854775808~9223372036854775807
unsigned long long80~18446744073709551615
float41.175494351e-38F~3.402823466e+38F
double82.2250738585072014e-308~1.7976931348623158e+308
long double16 
wchar_t4-2147483648~2147483647
所有指针8 
bool1true/false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cfloat>
#include <climits>
#include <iostream>

int main()
{
    printf("char:        %2ld, (%20d, %20d)\n", sizeof(char), CHAR_MIN, CHAR_MAX);
    printf("short:       %2ld, (%20d, %20d)\n", sizeof(short), SHRT_MIN, SHRT_MAX);
    printf("int:         %2ld, (%20d, %20d)\n", sizeof(int), INT_MIN, INT_MAX);
    printf("long:        %2ld, (%20ld, %20ld)\n", sizeof(long), LONG_MIN, LONG_MAX);
    printf("long long:   %2ld, (%20lld, %20lld)\n", sizeof(long long), LONG_LONG_MIN, LONG_LONG_MAX);
    printf("float:       %2ld, (%20f, %f)\n", sizeof(float), FLT_MIN, FLT_MAX);
    printf("double:      %2ld, (%20lf, %lf)\n", sizeof(double), DBL_MIN, DBL_MAX);
    // printf("long double: %2ld, (%20Lf, %Lf)\n", sizeof(long double), LDBL_MIN, LDBL_MAX);
    printf("wchar_t:     %2ld, (%20d, %20d)\n", sizeof(wchar_t), WCHAR_MIN, WCHAR_MAX);
    printf("pointer:     %2ld\n", sizeof(char*));
    printf("pointer:     %2ld\n", sizeof(int*));
    printf("bool:        %2ld\n", sizeof(bool));
}

3.2 对齐隐患

在不同系统因为对齐方式和大小可能不一样,对于强制类型转换就会出现问题,以及根据偏移取字段。

建议使用 #include <cstdint> 二次封装大小在不同位数系统上交互时。

1
2
3
4
5
6
7
8
#include <cstdint>

int16_t  a;
uint16_t ua;
int32_t  b;
uint32_t ub;
int64_t  c;
uint64_t uc;

3.3 指定对齐方式

1
2
#pragma pack(n)  // 设置n字节对齐
#pragma pack()   // 取消自定义字节对齐方式
特别是要根据变量类型偏移取下一个地址,这时候建议设置为 1 字节对齐。
  • 组建网络报文格式
  • 序列化和反序列化

在节约空间中将结构体前后添加 #pragma pack(1)#pragma pack() 即可得到完全紧凑排列。

image-20220612145032785

参考

  1. 深入理解字节对齐
  2. 理一理字节对齐那些事
本文由作者按照 CC BY 4.0 进行授权