何时复制填充字节 – 结构赋值,按值传递,其他?

在调试问题时,出现了以下问题。 (请忽略次要代码错误;代码仅供参考。)

定义了以下结构:

typedef struct box_t { uint32_t x; uint16_t y; } box_t; 

这个结构的实例正在通过函数的值传递(显然简化):

 void fun_a(box_t b) { ... use b ... } void fun_b(box_t bb) { // pass bb by value int err = funa(bb); } void fun_c(void) { box_t real_b; box_t some_b[10]; ... ... use real_b and some_b[] ... ... funb(real_b); funb(some_b[3]); ... box_t copy_b = some_b[5]; ... } 

在某些情况下,box_t的两个实例比较如下:

  memcmp(bm, bn, sizeof(box_t)); 

在几个嵌套调用中,box_t arg的字节使用以下内容转储:

 char *p = (char*) &a_box_t_arg; for (i=0; i < sizeof(box_t); i++) { printf(" %02X", *p & 0xFF); p++; } printf("\n"); 

sizeof(box_t)是8; 有2个填充字节(发现在uint16_t之后)。 转储显示结构的字段相等,但填充字节不是; 这导致memcmp失败(不足为奇)。

有趣的部分是发现’腐败’垫值的来源。 在向后跟踪之后,发现一些box_t实例被声明为局部变量并且被初始化为:

 box_t b; bx = 1; by = 2; 

上面没有(似乎)初始化pad字节,它似乎包含’garbage’(无论是在为b分配的堆栈空间中)。 在大多数情况下,使用memset(b, 0, sizeof(box_t))完成初始化。

问题是,通过(1)结构赋值或(2)传递值初始化box_t的实例是否总是相当于sizeof(box_t)的memcpy。 是不是只复制了“真实字段”的6个字节(并且填充字节不是)。

从调试开始,似乎总是完成memcpy sizeof(box_t)等价物。 有没有(例如,在标准中)实际指定这个? 随着调试的进行,了解有关pad字节处理的内容将会有所帮助。

谢谢! (在Ubuntu LTS 10.4 64位上使用GCC 4.4.3)

奖励积分:

 void f(void) { box_t ba; box_t bb; box_t bc; 

3个实例分配16个字节,而sizeof()显示8.为什么额外的空间?

填充字节的值未指定(C99 / C116.2.6.1§6):

当值存储在结构或联合类型的对象中(包括在成员对象中)时,对应于任何填充字节的对象表示的字节采用未指定的值。

另见脚注42/51(C99:TC3,C1x草案):

因此,例如,结构分配不需要复制任何填充比特。

编译器可以根据需要自由复制或不复制填充。 在x86 [1]上,我的猜测是将复制2个尾随填充字节,但不会复制4个字节(即使在32位硬件上也可能发生,因为结构可能需要8字节对齐,例如允许primefaces读取double值)。

[1]未进行实际测量。


扩大答案:

该标准不保证涉及填充字节的位置。 但是,如果使用静态存储持续时间初始化对象,则最终会出现归零填充的可能性很高。 但是如果你使用那个对象通过赋值来初始化另一个对象,那么所有的赌注都会再次关闭(并且我希望尾随填充字节 – 再次,没有进行测量 – 是特别好的候选者,可以从复制中省略)。

使用memset()memcpy() – 即使在分配给各个成员时,因为这也可以使填充无效 – 这是一种在合理的实现上保证填充字节值的方法。 但是,原则上编译器可以随时随意更改填充值( 可能与寄存器中的缓存成员相关 – 再次疯狂猜测),您可以通过使用volatile存储来避免这种情况。

我能想到的唯一合理的可移植解决方法是通过引入适当大小的虚拟成员来明确指定内存布局,同时使用特定于编译器的方法validation没有引入额外的填充( __attribute__ ((packed)) ,- -Wpadded for gcc)。

C11将允许您定义匿名结构和联合成员:

 typedef union box_t { unsigned char allBytes[theSizeOfIt]; struct { uint32_t x; uint16_t y; }; } box_t; 

该联合的行为与以前几乎相同,您可以访问.x等,但默认的初始化和赋值会发生变化。 如果您始终确保正确初始化变量,请执行以下操作:

 box_t real_b = { 0 }; 

或者像这样

 box_t real_a = { .allBytes = {0}, .x = 1, .y = 2 }; 

所有填充字节应正确初始化为0 。 如果您的整数类型具有填充位 ,这将无济于事,但至少您选择的uintXX_t类型根据定义将不具有它们。

gcc和粉丝已经实现了这个扩展,即使它们还没有完全是C11。

编辑:在P99中有一个宏以一致的方式执行此操作:

 #define P99_DEFINE_UNION(NAME, ...) \ union NAME { \ uint8_t p00_allbytes[sizeof(union { __VA_ARGS__ })]; \ __VA_ARGS__ \ } 

也就是说,数组的大小是通过声明一个“无标记”联合来确定的。

正如克里斯托夫所说,对填充没有任何保证。 你最好的选择是不使用memcmp比较两个结构。 它工作在错误的抽象级别。 memcmp在表示中按字节memcmp工作,而您需要比较成员的值。

更好地使用单独的比较函数,它采用两个结构并分别比较每个成员。 像这样的东西:

 int box_isequal (box_t bm, box_t bn) { return (bm.x == bn.x) && (bm.y == bn.y); } 

对于你的奖金,这三个对象是单独的对象,它们不是同一个数组的一部分,并且不允许它们之间的指针算术。 作为函数局部变量,它们通常在堆栈上分配,并且因为它们是分开的,所以编译器可以以最佳方式对齐它们,例如用于性能。