C++ 数据成员对齐和数组打包

Posted

技术标签:

【中文标题】C++ 数据成员对齐和数组打包【英文标题】:C++ Data Member Alignment and Array Packing 【发布时间】:2010-12-13 04:19:31 【问题描述】:

在代码审查期间,我遇到了一些定义简单结构的代码,如下所示:

class foo 
   unsigned char a;
   unsigned char b;
   unsigned char c;

在其他地方,定义了这些对象的数组:

foo listOfFoos[SOME_NUM];

稍后,这些结构被原始复制到缓冲区中:

memcpy(pBuff,listOfFoos,3*SOME_NUM);

此代码依赖于以下假设:a.) foo 的大小为 3,并且没有应用填充,b.) 这些对象的数组被打包,它们之间没有填充。

我已经在两个平台(RedHat 64b、Solaris 9)上使用 GNU 进行了尝试,并且在这两个平台上都可以使用。

以上假设是否有效?如果不是,在什么情况下(例如操作系统/编译器的更改)它们可能会失败?

【问题讨论】:

@Matthieu:感谢您提醒我们。我敢肯定 OP 忽略了这一点。 【参考方案1】:

这样做肯定更安全:

sizeof(foo) * SOME_NUM

【讨论】:

不仅更安全,而且更清晰,摆脱了一个神奇的数字。 +1 是的,我同意这一点。我想我更想了解填充和数组组织。谢谢。 这不考虑数组元素之间的填充。 见下面我的回答。最安全的方法是使用 sizeof(listOfFoos) @nschmidt: 在 C 或 C++ 中,数组元素之间不允许填充。【参考方案2】:

对象数组必须是连续的,因此对象之间永远不会有填充,尽管可以将填充添加到对象的末尾(产生几乎相同的效果)。

鉴于您使用的是 char,这些假设通常可能是正确的,但 C++ 标准当然不能保证这一点。不同的编译器,甚至只是更改传递给当前编译器的标志,都可能导致在结构的元素之间或在结构的最后一个元素之后插入填充,或两者兼而有之。

【讨论】:

如果编译器决定它喜欢四字节边界的东西,并在末尾添加一个字节的填充,我当然不会感到惊讶。【参考方案3】:

如果你像这样复制你的数组,你应该使用

memcpy(pBuff,listOfFoos,sizeof(listOfFoos));

只要您将 pBuff 分配给相同的大小,这将始终有效。 这样一来,您根本不需要对填充和对齐做出任何假设。

大多数编译器将结构或类与所包含的最大类型的所需对齐方式对齐。在您的字符情况下,这意味着没有对齐和填充,但是如果您添加一个短字符,例如您的类将是 6 个字节大,在最后一个字符和您的短字符之间添加一个字节的填充。

【讨论】:

【参考方案4】:

我认为这行得通的原因是结构中的所有字段都是 char 对齐一个。如果至少有一个字段不对齐 1,则结构/类的对齐方式不会为 1(对齐方式取决于字段顺序和对齐方式)。

让我们看一些例子:

#include <stdio.h>
#include <stddef.h>

typedef struct 
    unsigned char a;
    unsigned char b;
    unsigned char c;
 Foo;
typedef struct 
    unsigned short i;
    unsigned char  a;
    unsigned char  b;
    unsigned char  c;
 Bar;
typedef struct  Foo F[5];  F_B;
typedef struct  Bar B[5];  B_F;


#define ALIGNMENT_OF(t) offsetof( struct  char x; t test; , test )

int main(void) 
    printf("Foo:: Size: %d; Alignment: %d\n", sizeof(Foo), ALIGNMENT_OF(Foo));
    printf("Bar:: Size: %d; Alignment: %d\n", sizeof(Bar), ALIGNMENT_OF(Bar));
    printf("F_B:: Size: %d; Alignment: %d\n", sizeof(F_B), ALIGNMENT_OF(F_B));
    printf("B_F:: Size: %d; Alignment: %d\n", sizeof(B_F), ALIGNMENT_OF(B_F));

执行时,结果为:

Foo:: Size: 3; Alignment: 1
Bar:: Size: 6; Alignment: 2
F_B:: Size: 15; Alignment: 1
B_F:: Size: 30; Alignment: 2

您可以看到 Bar 和 F_B 的对齐方式为 2,因此其字段 i 将正确对齐。您还可以看到条形的大小是 6 而不是 5。同样,B_F 的大小(Bar 的 5)是 30 而不是 25

所以,如果你是硬编码而不是sizeof(...),你会在这里遇到问题。

希望这会有所帮助。

【讨论】:

看起来不错,不幸的是 offsetof 调用中的匿名结构在 msvc 2010 中无法编译【参考方案5】:

这一切都归结为内存对齐。典型的 32 位机器每次尝试读取或写入 4 个字节的内存。这种结构不会出现问题,因为它很容易低于 4 个字节,没有令人困惑的填充问题。

如果结构是这样的:

class foo 
   unsigned char a;
   unsigned char b;
   unsigned char c;
   unsigned int i;
   unsigned int j;

你的同事逻辑可能会导致

memcpy(pBuff,listOfFoos,11*SOME_NUM);

(3 个字符 = 3 个字节,2 个整数 = 2*4 个字节,所以 3 + 8)

不幸的是,由于填充结构实际上占用了 12 个字节。这是因为您不能将三个 char 和一个 int 放入该 4 字节字中,因此那里有一个字节的填充空间将 int 推入它自己的字中。数据类型越多样化,这个问题就越严重。

【讨论】:

【参考方案6】:

对于使用这样的东西并且我无法避免的情况,我会尝试在假设不再成立时使编译中断。我使用类似以下的内容(或Boost.StaticAssert,如果情况允许):

static_assert(sizeof(foo) <= 3);

// Macro for "static-assert" (only usefull on compile-time constant expressions)
#define static_assert(exp)           static_assert_II(exp, __LINE__)
// Macro used by static_assert macro (don't use directly)
#define static_assert_II(exp, line)  static_assert_III(exp, line)
// Macro used by static_assert macro (don't use directly)
#define static_assert_III(exp, line) enum static_assertion##linestatic_assert_line_##line = 1/(exp)

【讨论】:

【参考方案7】:

我认为我会安全并用 sizeof(foo) 替换魔术数字 3。

我的猜测是,针对未来处理器架构优化的代码可能会引入某种形式的填充。

而试图追踪这种错误是一件非常痛苦的事情!

【讨论】:

【参考方案8】:

正如其他人所说,使用 sizeof(foo) 是更安全的选择。一些编译器(尤其是嵌入式世界中深奥的编译器)会在类中添加一个 4 字节的头文件。根据您的编译器设置,其他人可以使用时髦的内存对齐技巧。

对于一个主流平台来说,你可能没问题,但不能保证。

【讨论】:

【参考方案9】:

当您在两台计算机之间传递数据时,sizeof() 可能仍然存在问题。其中一个代码可能会使用填充进行编译,而在另一个没有填充的情况下, sizeof() 会给出不同的结果。如果数组数据从一台计算机传递到另一台计算机,则会被误解,因为无法在预期的位置找到数组元素。 一种解决方案是确保尽可能使用#pragma pack(1),但这对于数组可能还不够。最好的办法是预见问题并使用填充到每个数组元素 8 字节的倍数。

【讨论】:

以上是关于C++ 数据成员对齐和数组打包的主要内容,如果未能解决你的问题,请参考以下文章

C++ SSE:存储到数组后的未定义行为

c++ - 如何使用`this`访问类数组中的成员变量

《C++语言基础》实践參考——数组作数据成员

C语言进阶自定义类型

内存对齐的原则

在 C 和 C++ 中对齐堆数组以简化编译器 (GCC) 向量化