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 64b、Solaris 9)上使用 GNU 进行了尝试,并且在这两个平台上都可以使用。

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

4

9 回答 9

22

这样做肯定会更安全:

sizeof(foo) * SOME_NUM
于 2009-11-04T20:29:18.943 回答
20

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

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

于 2009-11-04T20:30:54.487 回答
6

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

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

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

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

于 2009-11-04T20:40:22.790 回答
5

我认为这行得通的原因是结构中的所有字段都是对齐的字符。如果至少有一个字段不对齐 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 将正确对齐。您还可以看到 Size of Bar 是6 而不是 5。同样, B_F (Bar 的 5)的大小是30 而不是 25

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

希望这可以帮助。

于 2009-11-04T21:55:58.980 回答
2

我会很安全并用sizeof(foo)我认为的魔法数字 3 代替。

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

试图追查那种错误是一个真正的痛苦!

于 2009-11-04T20:30:48.457 回答
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 个字节。这是因为您不能将三个 char 和一个 int 放入该 4 字节字中,因此那里有一个字节的填充空间将 int 推入它自己的字中。数据类型越多样化,这就越是一个问题。

于 2009-11-04T20:41:57.390 回答
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)}
于 2010-03-09T12:54:40.617 回答
1

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

对于主流平台,您可能还可以,但不能保证。

于 2009-11-04T20:36:44.280 回答
0

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

于 2014-06-26T12:37:14.723 回答