C/C++ 结构体、类的内存对齐规则,以及#pragma pack、alignas、alignof的用法。
类和结构体的对齐规则完全一致,并且结构体也可以有虚函数表指针。本文使用结构体进行示例。
对齐规则
首先,编译器会有一个默认的字节对齐数,记为 n 1 \rm n_1 n1,取值为 1 , 2 , 4 , 8 , 16 1,2,4,8,16 1,2,4,8,16,比较常见的是取 8 8 8。
假设结构体中每个成员内存长度为 l i \rm l_i li, m a x ( l i ) \rm max(l_i) max(li)为最大的成员内存长度(对于结构体内有数组的情况, l i \rm l_i li 仍为单个数据类型元素的大小)。
对齐规则如下:
(1) 第一个数据成员的地址,与整个结构体对象的起始地址相同;数据成员的存储顺序,与成员的声明先后顺序一一对应。(如果有虚函数,则虚函数表指针为第一个数据成员)
(2) 结构体中每个成员相对于起始地址的偏移量,即对齐值应是
m
i
n
(
n
1
,
l
i
)
\rm min(n_1, l_i)
min(n1,li)的倍数。(若不满足,在成员之间进行填充)
(3) 结构体的总长度,应是
m
i
n
(
n
1
,
m
a
x
(
l
i
)
)
\rm min(n_1,max(l_i))
min(n1,max(li))的倍数。(若不满足,在结构体末尾进行填充)
接下来在64位系统中、编译器默认的对齐字节 n 1 = 8 \rm n_1 = 8 n1=8的情况下,对以上规则进行测试和验证:
规则(1)示例:
struct A
{
int a;
char b;
};
struct B
{
int a;
char b;
virtual int f(){}
};
A a;
B b;
cout << sizeof(a) << "\n"; //8
cout << sizeof(b) << "\n"; //16
cout << &a << endl; //0x7ffe32833f68
cout << &a.a << endl; //0x7ffe32833f68
cout << &b << endl; //0x7ffe32833f70
cout << &b.a << endl; //0x7ffe32833f78
分析:(1)A大小为8,B大小为16。B比A仅仅多了一个虚函数,因此b中含有虚函数表指针,所以b比a多一个指针的内存空间(64位系统为8字节)。
(2)a对象中第一个数据成员a.a的地址与a的起始地址相同。而b对象中,数据成员b.a的地址和b对象的起始地址相比,偏移了8个字节,这8个字节的内存正是因为虚函数表指针的存在,可以认为虚函数表指针作为第一个数据成员,从对象的起始地址开始存储。
规则(2)、规则(3)示例:
示例1:
struct A
{
char a[3];
short b;
};
A a;
cout << sizeof(a) << "\n"; //6
cout << &a.a << endl; //0x7ffd37436932
cout << &a.b << endl; //0x7ffd37436936
分析:根据规则(2)
char类型的数据类型长度 为1,因此对齐值为 m i n ( n 1 , l i ) = m i n ( 8 , 1 ) = 1 \rm min(n_1, l_i) = min(8, 1) = 1 min(n1,li)=min(8,1)=1,因此每一个字节依次存储即可
0x7ffd37436932
0x7ffd37436933
0x7ffd37436934
char a[3] 占用以上3个字节。
b为short类型,数据类型长度为2,对齐值为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
8
,
2
)
=
2
\rm min(n_1, l_i) = min(8, 2) = 2
min(n1,li)=min(8,2)=2,因此b存储的地址相对起始地址的偏移量,应是2的整数倍,而下一个地址0x7ffd37436935
相对起始的偏移量为3,3不能被2整除,补齐该字节后下个地址为4,4能够被2整除
0x7ffd37436936
0x7ffd37436937
short占用以上2个字节。
结构体的长度为 3 + 1(补齐) + 2 = 6。
根据规则(3),总长度应当为 m i n ( n 1 , m a x ( l i ) ) \rm min(n_1,max(l_i)) min(n1,max(li))的倍数, m a x ( l i ) \rm max(l_i) max(li)为short类型长度为2,因此 m i n ( n 1 , m a x ( l i ) ) = m i n ( 8 , 2 ) = 2 \rm min(n_1,max(l_i))=min(8,2)=2 min(n1,max(li))=min(8,2)=2,而6恰好是2的倍数,所以不需要再在末尾补齐。最终结构体总长度为6,存储方式如图。
示例2:
struct A
{
char a[3];
int* p;
short b;
};
A a;
cout << sizeof(a) << "\n"; //24
cout << &a.a << endl; //0x7ffd70217470
cout << &a.p << endl; //0x7ffd70217478
cout << &a.b << endl; //0x7ffd70217480
相比于上一个例子,在数组char a[3]
和short b
之间增加一个指针类型的数据成员,
根据规则(2),数组char a[3]
的存储方式同上,
而对于指针p
,其长度为8,对齐值为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
8
,
8
)
=
8
\rm min(n_1, l_i) = min(8, 8) = 8
min(n1,li)=min(8,8)=8,因此p
存储的地址相对起始地址的偏移量,应是8的整数倍;char a[3]
已经占了3个字节,因此需要再补齐5个字节,在相对起始地址偏移量为8的位置开始存储,在8 ~ 15的位置存储该指针。
此时已经占据了3 + 5(补齐) + 8 = 16个字节
对于数据b
,其长度为2,对齐值仍为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
8
,
2
)
=
2
\rm min(n_1, l_i) = min(8, 2) = 2
min(n1,li)=min(8,2)=2,下一个地址偏移量为16,恰好为2的整数倍,所以不需要补齐,直接存储即可,
此时已经占据了16 + 2 = 18个字节。
根据规则(3),此时结构体中最大成员长度为指针p
,占据8个字节,结构体的总长度应为
m
i
n
(
n
1
,
m
a
x
(
l
i
)
)
=
m
i
n
(
8
,
8
)
=
8
\rm min(n_1,max(l_i))=min(8,8)=8
min(n1,max(li))=min(8,8)=8的倍数,18不能被8整除,因此还需要补齐6个字节,18 + 6 = 24恰好能被8整除。最终结构体总长度为24,存储方式如图。
#pragma pack
上文中提到编译器的默认字节对齐数
n
1
\rm n_1
n1,该值可以使用#pragma pack(n1)
预处理命令进行设置和修改。
使用上文相同的例子,分别设置#pragma pack(4)
、#pragma pack(2)
进行对比
#pragma pack(4)的情况:
#pragma pack(4) //设置对齐字节数为4
struct A
{
char a[3];
int* p;
short b;
};
#pragma pack() //恢复默认为8
A a;
cout << sizeof(a) << "\n"; //16
此时结构体的大小为16,进行分析:
对于char a[3]
,存储方式不变,
对于int* p
,对齐值为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
4
,
8
)
=
4
\rm min(n_1, l_i) = min(4, 8) = 4
min(n1,li)=min(4,8)=4,因此存储的地址是4的整数倍即可,3不是2的整数倍,只需向后补齐1个字节,然后从4 ~ 11存储即可。
对于short b
,对齐值为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
4
,
2
)
=
2
\rm min(n_1, l_i) = min(4, 2) = 2
min(n1,li)=min(4,2)=2,12能被2整除,直接在12 ~ 13存储即可
此时已经占据了0 ~ 13 一共14个字节。
根据规则3,结构体的长度应被 m i n ( n 1 , m a x ( l i ) ) = m i n ( 4 , 8 ) = 4 \rm min(n_1,max(l_i))=min(4,8)=4 min(n1,max(li))=min(4,8)=4整除,14不能被4整除,再补齐2个字节,14 + 2 = 16能被4整除,因此最终的结构体总长度为16,存储方式如图。
#pragma pack(2)的情况:
#pragma pack(2) //设置对齐字节数为2
struct A
{
char a[3];
int* p;
short b;
};
#pragma pack() //恢复默认为8
A a;
cout << sizeof(a) << "\n"; //14
此时a的大小变为了14。进行分析
对于char a[3]
,存储方式不变,
对于int* p
,对齐值为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
2
,
8
)
=
2
\rm min(n_1, l_i) = min(2, 8) = 2
min(n1,li)=min(2,8)=2,因此存储的地址是2的整数倍即可,3不是2的整数倍,向后补齐1个字节,然后从4 ~ 11存储即可。
对于short b
,对齐值为
m
i
n
(
n
1
,
l
i
)
=
m
i
n
(
2
,
2
)
=
2
\rm min(n_1, l_i) = min(2, 2) = 2
min(n1,li)=min(2,2)=2,12能被2整除,从12 ~ 13存储即可
此时已经占据了0 ~ 13 一共14个字节。
根据规则3,结构体的长度应被 m i n ( n 1 , m a x ( l i ) ) = m i n ( 2 , 8 ) = 2 \rm min(n_1,max(l_i))=min(2,8)=2 min(n1,max(li))=min(2,8)=2整除,14能被2整除,不再需要在末尾补齐。因此最终的结构体总长度为14,存储方式如图。
alignas和alignof
alignas 和 alignof 是C++新增的关键字。
alignas的使用方法为:在结构体(类)名之前指定对齐字节数 n 2 \rm n_2 n2即可,例如:
struct alignas(4) A
{
...
};
在这里将alignas的对齐字节数记为
n
2
\rm n_2
n2,其与#pragma pack()
所指定的对齐字节数
n
1
\rm n_1
n1并不是一个概念。alignas指定对齐字节方式,是在编译器默认值(或者#pragma pack()指定值)自然对齐的基础上,再次进行对齐的。两者并不是并列或者互斥的关系,而是
n
2
\rm n_2
n2是依赖于
n
1
\rm n_1
n1的,两者的关系为:
n
2
=
m
a
x
(
n
2
,
m
i
n
(
n
1
,
m
a
x
(
l
1
)
)
)
\rm n_2 = max(n_2,min(n_1,max(l_1)))
n2=max(n2,min(n1,max(l1)))
可以看到,
m
i
n
(
n
1
,
m
a
x
(
l
1
)
)
\rm min(n_1,max(l_1))
min(n1,max(l1))正是规则(3)中结构体总长度需要被整除的值。
换句话说,alignas的作用仅仅影响了对齐规则的第三条(3)(末尾补齐规则),只需要将规则(3)改为:
(3) 结构体的总长度,应是 n 2 \rm n_2 n2 的倍数。(若不满足,在最后一个成员末尾进行填充)
而规则(1)和规则(2)不会受到alignas的影响。
相应的,alignof的返回值:(1)如果设置了alignas(n2)
,则返回n2的值。
(2)如果未设置alignas,alignof返回的正是
m
i
n
(
n
1
,
m
a
x
(
l
1
)
)
\rm min(n_1,max(l_1))
min(n1,max(l1))的值。
alignof的用法为:alignof(类名)
或者alignof(类对象)
使用同样的例子,进行验证:
#pragma pack(2)
,alignas(4)
时:
#pragma pack(2)
struct alignas(4) A
{
char a[3];
int* p;
short b;
};
#pragma pack()
cout << sizeof(A) << endl; //16
cout << alignof(A) << endl; //4
由以上例子可知,如果只设置#pragma pack(2)
的话,结构体长度为14。再加上alignas(4)
,要求结构体总长度为4的整数倍,因此由14补齐到16,
同理
#pragma pack(2)
,alignas(8)
时:
cout << sizeof(A) << endl; //16
cout << alignof(A) << endl; //8
#pragma pack(2)
,alignas(16)
时:
cout << sizeof(A) << endl; //16
cout << alignof(A) << endl; //16
两种情况的结构体大小均为16,因为补齐到16,被8 和 16均可整除,如下图:
注意,此时两种情况虽然结构体长度相同,但alignof(A)
的返回值不同,分别为8和16
#pragma pack(2)
,alignas(32)
,当align设置为32时,则需要补齐到32字节,如下图:
cout << sizeof(A) << endl; //32
cout << alignof(A) << endl; //32
此时结构体大小和alignof均为32
再看 #pragma pack(8)
,alignas(4)
的情况
#pragma pack(8)
struct alignas(4) A
{
char a[3];
int* p;
short b;
};
#pragma pack()
cout << sizeof(A) << endl; //24
cout << alignof(A) << endl; //8
m
i
n
(
n
1
,
m
a
x
(
l
1
)
)
=
m
i
n
(
8
,
8
)
=
8
\rm min(n_1,max(l_1)) = min(8,8) = 8
min(n1,max(l1))=min(8,8)=8 自然对齐的字节已经是8了,此时再设置alignas(4)
,无效。或者说,结构体长度已经能被8整除了,那么必然能被4整除,此时再设置alignas(4)
就是多此一举,alignof
并不会返回alignas
所设置的对齐值4,而是返回8。
同理,设置 #pragma pack(8)
,alignas(8)
时不变,
cout << sizeof(A) << endl; //24
cout << alignof(A) << endl; //8
设置 #pragma pack(8)
,alignas(16)
时,需要补齐到
m
a
x
(
16
,
m
i
n
(
8
,
8
)
)
=
16
max(16, min(8, 8)) = 16
max(16,min(8,8))=16的倍数,24不能被16整除,在24字节基础上再补齐8个字节,补齐到到32就能被16整除,因此:
cout << sizeof(A) << endl; //32
cout << alignof(A) << endl; //16
此时内存存储如下: