C/C++ 结构体、类的内存对齐规则,以及#pragma pack、alignas、alignof的用法。

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

此时内存存储如下:

在这里插入图片描述

  • 32
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值