C++结构体对齐补齐

本章内容

本章主要讲述在C++中,结构体存储数据内存对齐补齐的机制。

前情提要

最近写Pni-CT相关的网络通信时,发现在传输过程中,数据存在补零的现象,在多次调试后才发现原来不是因为网络通信导致数据补零,而是结构体存储数据本身的机制导致的。

结构体的好处

需要使用结构这种聚合数据类型来处理基本数据类型难以处理的场景。

为什么要内存对齐

  • 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。因为为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

#pragma pack

默认是#pragma pack(8),支持1,2,4,8,16。

例如:#pragma pack(3)会警告。

对齐补齐三原则

原则一:数据成员对齐规则

  • struct或union的数据成员,第一个数据成员放在offset为0的地方

  • 以后每个数据成员的对齐必须按照:#pragma pack(N)指定的N和这个数据成员自身长度,比较小的那个进行。

原则二:struct或union整体对齐规则

在数据成员完成各自对齐之后,struct或union本身也要对齐。对齐将按照#pragma pack(N)指定的N和struct或union最大数据成员长度中,比较小的那个进行。

原则三:struct或union成员对齐原则

如果一个struct或union里有struct或union成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

示例

环境

Windows、g++、Emacs(编辑器)

gcc version 11.2.0 (Rev2, Built by MSYS2 project)

int 4字节

char 1字节

float 4字节

double 8字节

按1对齐

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

int main()
{
//按1对齐
#pragma pack(1)
struct A{
int a;
char b;
short c;
char d;
};
A test;
cout << "sizeof(A):\t"<<sizeof(A) << endl;
cout <<(int*)&test.a << ", " << (int*)&test.b << ", " << (int*)&test.c << ", " << (int*)&test.d << endl;
return 0;
}

输出结果

分析

结构体A遵循原则一和二。

  • 原则一,成员对齐。

    成员a,4字节,#pragma pack(1),4>1,按1对齐,offset为0,存放区间[0,3];

    成员b,1字节,1=1,按1对齐,offset = 4,存放区间[4];

    成员c,2字节,2>1,按1对齐,offset = 5,存放区间[5,6];

    成员d,1字节,1=1,按1对齐,offset = 6,存放区间[7];

  • 原则二,struct整体对齐。

    整体存放在[0,7]中,共八字节;对齐 pack(1),8是1的倍数,就是八字节。

按2对齐

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

int main()
{
//按2对齐
#pragma pack(2)
struct B{
int a;
char b;
short c;
char d;
};
B test;
cout << "sizeof(B):\t"<<sizeof(B) << endl;
cout <<(int*)&test.a << ", " << (int*)&test.b << ", " << (int*)&test.c << ", " << (int*)&test.d << endl;
return 0;
}

运行结果

分析

结构体B遵循原则一和二。

  • 原则一,成员对齐。

    成员a,4字节,#pragma pack(2),4>2,按2对齐,offset为0,存放区间[0,3]

    成员b,1字节,1<2,按1对齐,offset = 4,存放区间[4];

    成员c,2字节,2=2,按2对齐,offset = 5,此时的offset应该被对齐数2整除,由5变6,存放区间[6,7];

    成员d,1字节,1<2,按1对齐,offset = 8,存放区间[8];

  • 原则二,struct整体对齐。

    整体存放在[0,8]中,共9个字节;对齐 pack(2),应该被2整除,就是10个字节。

按4对齐

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

int main()
{
//按4对齐
#pragma pack(4)
struct C{
int a;
char b;
short c;
char d;
};
C test;
cout << "sizeof(C):\t"<<sizeof(C) << endl;
cout <<(int*)&test.a << ", " << (int*)&test.b << ", " << (int*)&test.c << ", " << (int*)&test.d << endl;
return 0;
}

运行结果

分析

结构体C遵循原则一和二。

  • 原则一,成员对齐。

    成员a,4字节,#pragma pack(4),4=4,按4对齐,offset为0,存放区间[0,3];

    成员b,1字节,1<4,按1对齐,offset = 4,存放区间[4];

    成员c,2字节,2<4,按2对齐,offset = 5,此时的offset应该被对齐数2整除,由5变6,存放区间[6,7];

    成员d,1字节,1<4,按1对齐,offset = 8,存放区间[8];

  • 原则二,struct整体对齐。

    整体存放在[0,8]中,共9个字节;对齐 pack(4),应该被4整除,就是12个字节。

按8对齐

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

int main()
{
//按8对齐
#pragma pack(8)
struct D{
int a[2];
short b[3];
double c;
};
D test;
cout << "sizeof(D):\t"<<sizeof(D) << endl;
cout <<(int*)&test.a << ", " << (int*)&test.b << ", " << (int*)&test.c << endl;
return 0;
}

运行结果

分析

结构体D遵循原则一和二。

  • 成员a,单体4字节,4<8,按4对齐,offset = 0,存放区间[0,7];

    成员b,单体2字节,2<8,按2对齐,offset = 8,存储区间[8,13];

    成员c,8字节,8 = 8,按8对齐,offset = 14,此时的offset应该被对齐数8整除,由14变16,存储区间[16, 23];

  • 原则二,struct整体对齐。

    整体存放在[0,23]中,共24个字节;对齐 pack(8),可以被8整除,就是24个字节。

嵌套结构体按4对齐

代码

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
#include <iostream>
using namespace std;

int main()
{
//嵌套结构体,按4对齐。
#pragma pack(4)
struct E
{
char g[2];
short h;
struct F
{
int a;
double b;
float c;
};
F f;
};

E test;
cout << "sizeof(E):\t" << sizeof(E) << endl;
//cout <<(int*)&test.a << ", " << (int*)&test.b << ", " << (int*)&test.c << endl;
cout << (int*)&test.g << ", " << (int*)&test.h << ", " <<(int*)&test.f.a << ", " << (int*)&test.f.b << ", " << (int*)&test.f.c << endl;
return 0;
}

运行结果

分析

嵌套结构,顺序计算。

  • 原则一、二

    成员g单体1字节,#pragma pack(4),4>1,按1对齐,offset = 0,存放区间[0,1];

    成员h二字节,2<4,按2对齐,offset = 2,存放区间[2,3];

  • 此时,应用原则三

    offset=4,结构体内部最大元素为double(8字节),offset应该被8整除,所以offset变为8,但是原则一,#pragma pack(4),4<8,所以还是从4开始存放接下来的struct F(f)

  • 结构体F,遵循原则一和二

    成员a,4字节,#pragma pack(4),4=4,按4对齐,offset = 4,存放区间[4,7];

    成员b,8字节,8>4,按4对齐,offset = 8,存储区间[8,15];

    成员c,4字节,4=4,按4对齐,offset = 16,存储区间[16,19];

  • 原则二,struct整体对齐。

    整体存放在[0,19]中,共20个字节;对齐 pack(4),可以被4整除,就是20个字节。

嵌套结构体按8对齐

代码

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
#include <iostream>
using namespace std;

int main()
{
#pragma pack(8)
struct G
{
int a;
char b[3];
short c[3];
double d[2];

struct H
{
int e;
float f;
double g;
};
H h;
};

G test;
cout << "sizeof(G):\t" << sizeof(G) << endl;
cout << (int*)&test.a << ", " << (int*)&test.b << ", " <<(int*)&test.c << ", " << (int*)&test.d << ", " << (int*)&test.h.e << ", " << (int*)&test.h.f << ", " << (int*)&test.h.g << endl;
return 0;
}

运行结果

分析

嵌套结构,顺序计算。

  • 原则一、二。

    成员a,4字节,#pragma pack(8),4<8,按4对齐,offset为0,存放区间[0,3];

    成员b,单体1字节,1<8,按1对齐,offset = 4,存放区间[4,6];

    成员c,单体2字节,2<8,按2对齐,offset = 7,此时的offset应该被对齐数2整除,由7变8,存放区间[8,13];

    成员d,单体8字节,8=8,按8对齐,offset = 14,此时的offset应该被对齐数8整除,由14变16,存放区间[16,31];

  • 原则三:offset = 32,最大成员为double,32%8==0,那么H的成员从32开始存。

    成员e,4字节,4<8,按4对齐,offset = 32,存放区间[32,35];

    成员f,4字节,4<8,按4对齐,offset = 36,存放区间[36,39];

    成员g,8字节,8=8,按8对齐,offset = 40,存放区间[40,47];

  • 原则二:struct整体对齐。

    整体存放在[0,47]中,共48个字节;对齐 pack(8),可以被8整除,就是48个字节。

总结

  • #Pragma Pack(N);只影响的是数据在内存中的排列方式。
  • cpu可以访问任何pack的数据,只是效率有高低之分。
  • 最好把大小相似的数据元素(如整型、浮点型)放到开头;把奇数大小的数据(如字符串、缓冲区)放到后面。