结构体内存对齐


一、什么是结构体内存对齐

进入讲解前,先看一段 C 代码:

struct Node1 {
    int a;
    char b;
    char c;
} node1[1024];

struct Node2 {
    char b;
    int a;
    char c;
} node2[1024];

思考一下 node1node2 的大小分别为多少?

printf("The size of node1 is: %d\r\n", sizeof(node1));
printf("The size of node2 is: %d\r\n", sizeof(node2));

结果如下:

The size of node1 is: 8192
The size of node2 is: 12288

我是在 Windows 下 MinGW32 的 GCC 测试的

一样的成员属性,但 node1 只有 8K,而 node2 的大小却有 12K。

由此可见,结构体对齐,实质上就是内存对齐。

二、为什么要结构体内存对齐

为什么要结构体对齐,原因就是内存要对齐,原因是芯片内存的制造限制,是制造成本约束,是内存读取效率要求。

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,

  • 为了访问未对齐的内存,处理器需要作两次内存访问;
  • 对齐的内存访问仅需要⼀次访问。

假设一个处理器总是从内存中取 8 个字节,则地址必须是 8 的倍数。如果我们能保证将所有的 double 类型的数据的地址都对齐成 8 的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个 8 字节内存块中。

总体来说,结构体的内存对齐是拿空间来换取时间的做法。

三、对齐系数

对齐系数是指编译器在内存布局中对结构体成员进行对齐的要求。由于硬件访问内存的方式有一定的要求,结构体的对齐方式会影响内存的使用效率和访问速度。

在大多数编译器中,结构体对齐系数是通过编译器的编译选项或者特定的指令来指定的。对齐系数通常以字节为单位,表示结构体成员的起始位置必须是该字节数的整数倍。

注意,不同的平台,模数是不一样的。

struct Node3 {
	int a;
	double b;
} node3;

printf("The size of node3 is: %d\r\n", sizeof(node3));
printf("node3.a address: %p\r\n", &node3.a);
printf("node3.b address: %p\r\n", &node3.b);

结果如下:

The size of node3 is: 16
node3.a address: 003CF1B8
node3.b address: 003CF1C0

可知,对齐系数就是 8.

当然,对齐模数是可以改变的,可以用预编译命令 #pragma pack(n),n=1,2,4,8,16(必须是 2 的幂次方)来改变这一系数,其中的 n 就是你要指定的“对齐系数”。

现在将上面的结构体改变一下:

#pragma pack(4)
struct Node3 {
	int a;
	double b;
} node3;
#pragma pack()

将它的对齐模数改为了 4,结果如下:

The size of node3 is: 12
node3.a address: 0042F1B8
node3.b address: 0042F1BC

四、结构体对齐规则

  1. 结构体的内存大小,并非其内部元素大小之和;
  2. 结构体变量的起始地址,可以被最大元素基本类型大小或者模数整除;
  3. 结构体的内存对齐,按照其内部最大元素基本类型或者模数大小对齐;
  4. 模数在不同平台值不一样,也可通过 #pragma pack(n) 方式去改变,其中 n 一定是 2 的幂次方,如 1,2,4,8,16 等;
  5. 如果空间地址允许,结构体内部元素会拼凑一起放在同一个对齐空间;
  6. 结构体内有结构体变量元素,其结构体并非展开后再对齐;
  7. unionbitfield 变量也遵循结构体内存对齐原则。

下面结合例子来看一下:

1、例一:文章开头的例子

对于本文一开始提到的例子,它们的内存对齐方式如下(这里就不以数组举例了):

struct Node1 {
	int a;
	char b;
	char c;
} node1;

struct Node2 {
	char b;
	int a;
	char c;
} node2;

解释如下:

node1 中的元素 a 是 int 类型,按 4 个字节对齐,其地址位是 4 的整数倍;而 b 和 c 就按 1 字节对齐,就跟在 a 后面就行了。

node2 中 b 是按 1 个字节对齐,放在最前面;而 a 是按 4 个字节对齐,其地址位必须是 4 的整数倍,所以,只能找到个 +4 的位置;紧接着 c 就按 1 字节对齐,跟其后面。

2、例二:稍微复杂的情况

struct Node4 {
	char  a;
	short b;
	char  c;
	int   d;
	char  e;
} node4;

结果如下:

The size of node4 is: 16
node4.a address: 00E1F1C4
node4.b address: 00E1F1C6
node4.c address: 00E1F1C8
node4.d address: 00E1F1CC
node4.e address: 00E1F1D0

其内存分布如下:

3、结合 union 和 struct

struct Node5 {
	int a;        
	union {
		char ua[9];      
		int ub;
	}u;      
	double b;  
	int c;
} node5;

如果 union 里的元素类型不一样,那就以最大长度的那个类型对齐了。

4、结构体嵌套

typedef struct {
	int a;
	char b;
} node1;

struct Node11 {
	node1 nd1;
	char c;
} node11;

typedef struct {
	short a;
	char b;
} node2;

struct Node22 {
	node2 nd2;
	char c;
} node22;
The size of node11 is: 12
node11.nd1 address: 0028F1D4
node11.c address: 0028F1DC

The size of node22 is: 6
node22.nd2 address: 0028F1E0
node22.c address: 0028F1E4

可以看见,结构体内的结构体,结构体内的元素并不会和结构体外的元素合并占一个对齐单元。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值