我制作了以下代码作为示例。
#include <iostream>
struct class1
{
uint8_t a;
uint8_t b;
uint16_t c;
uint32_t d;
uint32_t e;
uint32_t f;
uint32_t g;
};
struct class2
{
uint8_t a;
uint8_t b;
uint16_t c;
uint32_t d;
uint32_t e;
uint64_t f;
};
int main(){
std::cout << sizeof(class1) << std::endl;
std::cout << sizeof(class2) << std::endl;
std::cout << sizeof(uint64_t) << std::endl;
std::cout << sizeof(uint32_t) << std::endl;
}
打印
20
24
8
4
因此,很容易看出一个uint64_t和两个uint32_t一样大,为什么类2会有4个额外的字节,如果它们是相同的,除了用两个uint32_t代替一个uint64_t。
如前所述,这是由于填充造成的。
为了防止这种情况,您可以使用
#pragma pack(1)
class ... {
};
#pragma pack(pop)
它告诉编译器不要对齐到8个字节,而是对齐到一个字节。pop命令将其关闭(这一点非常重要,因为如果您在标头中执行此操作,并且有人包含您的标头,则可能会出现非常奇怪的错误)
在类中使用uint64_t时,为什么它需要比2个uint32_t更多的内存?
原因是由于对齐要求而进行填充。
在大多数64位架构上,uint8_t具有1的对准要求,uint16_t具有2的对准要求、uint32_t具有4的对准要求以及uint64_t具有8的对准要求。编译器必须确保结构中的所有成员都正确对齐,并且结构的大小是其整体对齐要求的倍数。此外,编译器不允许对成员重新排序。
因此,你的结构最终布局如下
struct class1
{
uint8_t a; //offset 0
uint8_t b; //offset 1
uint16_t c; //offset 2
uint32_t d; //offset 4
uint32_t e; //offset 8
uint32_t f; //offset 12
uint32_t g; //offset 16
}; //overall alignment requirement 4, overall size 20.
struct class2
{
uint8_t a; //offset 0
uint8_t b; //offset 1
uint16_t c; //offset 2
uint32_t d; //offset 4
uint32_t e; //offset 8
// 4 bytes of padding because f has an alignment requirement of 8
uint64_t f; //offset 16
}; //overall alignment requirement 8, overall size 24
如何防止这种情况发生?
不幸的是,没有一个好的通用解决方案。
有时可以通过重新排序字段来减少填充量,但这对您的情况没有帮助。它只是在结构中移动填充。具有需要8字节对齐的字段的结构将始终具有8的倍数大小。因此,无论您如何重新排列字段,您的结构都将始终具有至少24的大小。
您可以使用编译器特定的功能(如#pragma pack
或__attribute((packed))
)来强制编译器将结构封装得比正常对齐要求所允许的更紧密。然而,这不仅限制了可移植性,而且在获取成员地址或绑定对该成员的引用时也会产生问题。由此产生的指针或引用可能不满足对齐要求,因此使用起来可能不安全。
不同的编译器处理这个问题的方式各不相同。来自于一些在godbolt上玩耍的人。
- g++9到11将拒绝绑定对打包成员的引用,并在获取地址时发出警告
- clang 4到11在获取地址时会发出警告,但会静默地绑定引用并将该引用传递到编译单元边界
- Clang 3.9及更早版本将获取地址并静默地绑定引用
- g++8及更早版本和clang3.9及更早版本(godbolt上最旧的版本)也将拒绝绑定引用,但会在没有警告的情况下获取地址
- icc将绑定指针或获取地址,而不会在任何情况下产生任何警告(尽管公平地说,英特尔处理器在硬件中支持未对齐的访问)
对齐规则(在x86和x86_64上)通常是根据变量的大小对齐。
换句话说,32位变量在4字节上对齐,64位变量在8字节上对齐等。
f
的偏移量是12,因此在uint32_t f
的情况下不需要填充,但当f
是uint64_t
时,添加4个字节的填充以使f
在8个字节上对齐。
因此,最好按数据成员从大到小的顺序排列。然后就不需要任何填充或包装了(除了可能在结构的末尾)。