前言
在c 中,结构体(struct)的对齐是指编译器为确保结构体成员在内存中的地址满足特定的对齐要求而进行的调整。对齐有助于提高访问速度,但同时也可能导致内存空间的浪费。
一、为什么要对结构体进行对齐操作?
结构体对齐的主要目的是优化内存访问速度和提高内存利用率。
二、基本概念
对齐(alignment):指数据在内存中按照某种边界方式排列。例如,4字节的整数通常按4字节边界对齐。
填充(padding):为了满足对齐要求而在数据之间插入的无意义的字节。
边界(alignment boundary):存储单元的起始地址必须是其大小的整数倍,例如,4字节的整数应该存储在能被4整除的地址上。
三、 对齐规则
编译器通常遵循以下规则来对齐结构体:
- 每个成员变量的地址都是其自身大小的整数倍。
- 结构体的总大小是其最大成员大小的整数倍。
- 编译器可能会插入填充字节以满足上述对齐要求。
四、示例讲解
1.简单的变量对齐
struct example {
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
};
编译器将如何对齐和填充这个结构体?假设默认对齐是4字节:
char a
占用1字节,开始地址0x00。int b
需要4字节对齐,下一个可以被4整除的地址是0x04,因此在a
后面插入3个填充值。short c
需要2字节对齐,b
占用4个字节后,c
可以直接放在下一个地址0x08。
所以结构体在内存中的布局如下:
以结构体在内存中的布局如下:
地址 | 内容 |
---|---|
0x00 | a |
0x01 | 填充 |
0x02 | 填充 |
0x03 | 填充 |
0x04 | b |
0x05 | b |
0x06 | b |
0x07 | b |
0x08 | c |
0x09 | c |
总大小是10字节,但为了满足最大成员(int
)的对齐要求,编译器可能会将结构体的总大小调整为12字节,使其成为4的倍数。
2.结构体包含有结构体的对齐
编译器将根据每个类型的自然对齐需求来安排各个成员的位置,以便优化内存访问。
#include
struct innerstruct {
char a; // 1字节
int b; // 4字节
};
struct outerstruct {
char c; // 1字节
innerstruct inner; // 包含内部结构体
double d; // 8字节
};
结构体成员详细解析
-
内部结构体
innerstruct
:char a
:占用1字节。int b
:通常情况下,占用4字节。
-
外部结构体
outerstruct
:char c
:占用1字节。innerstruct inner
:包含了innerstruct
类型的内部结构体。double d
:占用8字节。
-
内部结构体
innerstruct
的对齐:char a
占用1字节。由于int b
需要4字节对齐,所以a
后面会有3字节的填充,使得b
能够对齐到下一个4的倍数地址上。- 因此,
innerstruct
的总大小将是8字节(1字节a
3字节填充 4字节b
)。
-
外部结构体
outerstruct
的对齐:char c
占用1字节。为了对齐innerstruct
,c
后面会有3字节的填充。innerstruct inner
将从4字节边界开始,总共占用8字节(如上所述)。double d
通常需要8字节对齐,因此在innerstruct inner
后面可能会有额外的填充字节。
下面是这两个结构体在默认对齐规则下的内存布局示意图:总结20字节
outerstruct:
| c (1b) | padding (3b) | innerstruct (8b) | double d (8b) |
innerstruct:
| a (1b) | padding (3b) | b (4b) |
五、使用指令改变对齐方式
可以使用编译器特定的指令或预处理指令来改变结构体的对齐方式。
__attribute__((packed))
例如,在gcc中,可以使用 __attribute__((packed))
来消除填充:
struct examplepacked {
char a;
int b;
short c;
} __attribute__((packed));
在msvc中,可以使用 #pragma pack(push, n)
和 #pragma pack(pop)
:
#pragma pack(push, 1)
struct examplepacked {
char a;
int b;
short c;
};
#pragma pack(pop)
#pragma pack(push, n)
- 功能:
#pragma pack(push, n)
将当前的对齐设置压入一个栈中,并将对齐值设置为n
。 - 参数:
n
:指定新的对齐值(通常是1、2、4、8或16),表示数据成员应该按照n
字节边界进行对齐。
#pragma pack(pop)
- 功能:
#pragma pack(pop)
用于从栈中弹出之前保存的对齐设置,并恢复该设置。
这样做的结果是结构体成员紧密排列,没有填充字节,但会导致性能下降,因为访问未对齐的数据可能需要更多的处理时间。
六、总结
- 默认对齐:通常情况下,最好使用编译器的默认对齐方式,以获得最佳性能。
- 手动对齐:只有在明确知道需要节省内存,并且能接受性能损失的情况下才使用手动对齐。
理解和控制结构体的对齐对于编写高效、可靠的c 程序非常重要。希望这篇详解能够帮助你更好地掌握结构体对齐的概念和应用。