为什么在类中使用时,uint64_t需要的内存超过 2 uint32_t?以及如何防止这种情况发生?



我制作了以下代码作为示例。

#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的情况下不需要填充,但当fuint64_t时,添加4个字节的填充以使f在8个字节上对齐。

因此,最好按数据成员从大到小的顺序排列。然后就不需要任何填充或包装了(除了可能在结构的末尾)。

相关内容

  • 没有找到相关文章

最新更新