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

17

在进行代码审查时,我看到了一些定义简单结构的代码:

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 64位,Solaris 9)尝试过,并且在两个平台上都可以运行。

以上假设是否有效?如果无效,那么在什么条件下(例如更改操作系统/编译器)可能会失败?


@Matthieu:感谢您提醒我们。我相信原帖作者可能忽略了这一点。 - user1115652
9个回答

22

最好的做法是:

sizeof(foo) * SOME_NUM

3
不仅更安全,而且更清晰,还摆脱了一个神奇的数字。+1 - rmeador
是的,我同意。我想我更多地是在试图探讨填充和数组组织方面。谢谢。 - Adam Holmberg
1
这并不考虑数组元素之间的填充。 - nschmidt
1
最安全的方法是使用sizeof(listOfFoos)。 - nschmidt
3
在 C 和 C++ 中都不允许在数组元素之间添加填充。 - Jerry Coffin
@Jerry Coffin:你是正确的。标准要求数组是连续的。填充仅在结构体/类中发生。 - nschmidt

20

要求对象数组是连续的,因此对象之间永远没有填充,尽管可以在对象末尾添加填充(产生几乎相同的效果)。

考虑到您正在使用char,这些假设可能更多地是正确的,但C++标准并不保证它。不同的编译器,甚至只是当前编译器传递的标志的变化,都可能导致结构体元素之间或结构体的最后一个元素之后插入填充,或两者同时出现。


1
如果编译器决定将内容放在4字节边界上,并在末尾添加一个字节的填充,我也不会感到惊讶。 - David Thornley
我知道这是一个老问题,但我想知道:在C++标准的哪里规定了可以在对象的末尾添加填充,而不是开头?我读到过它必须是连续的,并且一个数组new-expressing可能会分配比所需更多的空间,但我找不到任何信息,例如数组对象和第一个元素具有相同的地址。[basic.compound]注意4说这一点,但似乎并不是一个要求。标准似乎没有清晰明确的保证。 - JHBonarius
1
@JHBonarius:当前标准只针对标准布局对象和非位域成员提供此保证。规范文本在[class.mem]/26中:“如果标准布局类对象有任何非静态数据成员,则其地址与其第一个非静态数据成员的地址相同,如果该成员不是位域。” - Jerry Coffin

6
如果您像这样复制您的数组,应该使用:
memcpy(pBuff,listOfFoos,sizeof(listOfFoos));

只要您将pBuff分配给相同的大小,这种方法始终有效。这样,您就不会对填充和对齐作出任何假设。
大多数编译器将结构体或类对齐到最大类型所需的对齐方式。在您使用字符的情况下,这意味着没有对齐和填充,但是如果您添加一个short,例如您的类将变为6个字节,最后一个char和short之间添加了一个字节的填充。

5

我认为这个方法有效是因为结构体中的所有字段都是char类型,它们对齐在一起。如果至少有一个字段不对齐,则结构体/类的对齐方式将不是1(对齐方式将取决于字段顺序和对齐方式)。

让我们看一些例子:

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

typedef struct {
    unsigned char a;
    unsigned char b;
    unsigned char c;
} Foo;
typedef struct {
    <b>unsigned short i;</b>
    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 将被正确对齐。你也可以看到 Bar 的大小为 6 而不是 5。类似地,B_F (Bar 的 5) 的大小是 30 而不是 25
因此,如果你使用硬编码代替 sizeof(...),在这里将会有问题。
希望这可以帮到你。

看起来很不错,但是在MSVC 2010中,offsetof调用内部的匿名结构无法编译。 - user1115652

2

对于像这样使用的情况,我无法避免它,我会尝试在前提不再成立时使编译失败。我会使用类似以下代码(或者如果情况允许,则使用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##line{static_assert_line_##line = 1/(exp)}

2

关键在于内存对齐。典型的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个字节。这是因为你不能将三个字符和一个整数放入那个4字节的单词中,所以有一个字节的填充空间将整数推入它自己的单词中。随着数据类型越来越多样化,这变得越来越成问题。


2

我想,如果使用sizeof(foo)代替魔数3,代码就会更加安全。

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

而且,试图追踪这种错误真是一件令人头疼的事情!


1
如其他人所说,使用 sizeof(foo) 更为安全。某些编译器(尤其是嵌入式领域中的奇特编译器)会对类添加一个 4 字节的标头。其他编译器可能会根据您的编译器设置执行奇怪的内存对齐技巧。
对于主流平台而言,您可能没问题,但这并非保证。

0

在两台计算机之间传递数据时,sizeof()可能仍然存在问题。其中一台计算机上的代码可能会编译带有填充,而另一台计算机则没有,这种情况下sizeof()将给出不同的结果。如果数组数据从一台计算机传递到另一台计算机,则会被错误解释,因为数组元素将不会在预期位置找到。

一个解决方案是确保尽可能使用#pragma pack(1),但这对于数组可能不足够。最好的方法是预见问题并对每个数组元素使用8字节的填充。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接