C++ 结构体对齐问题

8

我有一些预定义的结构体(实际上有几个),其中变量横跨32位字边界。在Linux(以及使用GCC的Windows)中,我能够使用“attribute((packed))”使我的结构体正确地打包到正确的大小。然而,我无法通过VC++和#pragma pack以相同的方式使其工作。

使用GCC,这将返回正确的6字节大小:

struct
{
    unsigned int   a                : 3;
    unsigned int   b                : 1;
    unsigned int   c                : 15;
    unsigned int   troubleMaker     : 16;
    unsigned short padding          : 13;
} __attribute__((packed)) s;

使用VC++,这将返回不正确的大小为8个字节。
#pragma pack(push)
#pragma pack(1)

struct
{
    unsigned int   a                : 3;
    unsigned int   b                : 1;
    unsigned int   c                : 15;
    unsigned int   troubleMaker     : 16;
    unsigned short padding          : 13;
} s;

#pragma pack(pop)

我可以手动跨越边界来使“troubleMaker”正常工作,但我不想这样做。有什么建议吗?


2
它们不仅跨越32位边界,还跨越单字节边界。要获得大小为6,变量必须从字节中间开始。我很惊讶GCC允许这样做。无论如何,如果我是你,我会放弃位域,并只使结构体包含一个6个字符(或3个shorts或其他)的数组,然后编写访问器函数来屏蔽所需的位。 - jalf
6个回答

17

疯狂的想法: 一开始就编写遵循C99或C++03标准的程序


我建议不要使用特定于供应商的C语言扩展来匹配设备或网络位格式。即使您使用一系列每个供应商语言扩展中的一个来使字段对齐,仍然需要考虑字节顺序,并且仍然需要额外的指令来访问结构体布局。

您可以编写符合C99标准的程序,通过使用标准化的C API字符串和内存复制函数以及Posix hton和ntoh函数,在任何架构或主机上以最大速度和缓存效率运行。

一个好的实践是使用以下存在已发布标准的函数:

C99: memcpy(), Posix: htonl(), htons(), ntohl(), ntohs()

更新: 这里有一些代码应该在所有地方都可以工作。如果Microsoft 仍然没有为C99实现它,你可能需要从这个项目获取<stdint.h>,或者只是对int的大小做出通常的假设。

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>

struct packed_with_bit_fields {  // ONLY FOR COMPARISON
    unsigned int   a        : 3;
    unsigned int   b        : 1;
    unsigned int   c        : 15;
    unsigned int   troubleMaker : 16;
    unsigned short padding  : 13;
} __attribute__((packed));       // USED ONLY TO COMPARE IMPLEMENTATIONS

struct unpacked { // THIS IS THE EXAMPLE STRUCT
    uint32_t a;
    uint32_t b;
    uint32_t c;
    uint32_t troubleMaker;
}; // NOTE NOT PACKED

struct unpacked su;
struct packed_with_bit_fields sp;
char *bits = "Lorem ipsum dolor";

int main(int ac, char **av) {
  uint32_t x;   // byte order issues ignored in both cases

  // This should work with any environment and compiler
  memcpy(&x, bits, 4);
  su.a = x & 7;
  su.b = x >> 3 & 1;
  su.c = x >> 4 & 0x7fff;
  memcpy(&x, bits + 2, 4);
  su.troubleMaker = x >> 3 & 0xffff;

  // This section works only with gcc
  memcpy(&sp, bits, 6);
  printf( sp.a == su.a
      &&  sp.b == su.b
      &&  sp.c == su.c
      &&  sp.troubleMaker == su.troubleMaker
      ? "conforming and gcc implementations match\n" : "huh?\n");
  return 0;
}

考虑到它被标记为C++,编写符合C++03标准的程序可能是一个更好的选择。 ;) - jalf
好的,说得不错,但它也标记了gcc,使用的是C89代码,如果微软真的有一个C编译器,那可能就是真正的目标了。 :-) - DigitalRoss
我之所以尝试这种方式,是因为我正在使用别人编写的遗留代码。到目前为止,我们一直很幸运,但一些新消息导致了这个问题。我相当确定它需要重新思考,但我想看看是否有人对如何使其按原样工作有任何有用的建议... - NJChim
2
当然,系统工作通常是遗留代码,我很同情。我只是以这种方式编写标题,以便吓唬任何阅读它的新人编写符合规范的代码,以便下一个可怜的人来维护。 :-) - DigitalRoss
1
我不明白你发布的代码为什么可以在所有情况下运行,因为它使用了__attribute__((packed)),而这正是我遇到问题的具体原因...这在vc++编译器中不存在。 - NJChim
请注意,紧凑结构仅在“// 仅适用于gcc”部分中使用,并且仅用于在程序末尾进行比较,以显示两个实现产生相同的最终结果。 - DigitalRoss

7

位域的对齐和排序因实现而异,这是众所周知的。声明一个普通整数字段并使用掩码和按位(| & ^)运算符在其中操作“位域”要更加安全。


足够正确。位域的行为在编译器上没有标准化。位域在小端和大端架构上的行为往往不同。位域非常适合用于内部状态,但由于此原因,在交换数据方面并不是很好。 - Adriaan

2

我不相信Visual Studio支持这种行为。除了尝试使用__declspec(align(1))宏包之外,我还尝试过其他方法,但是得到了相同的结果。我认为你只能使用12个字节或者稍微调整一下你的结构。


1
谢谢您的建议,但重新排序并不是一个选项,因为我的结构体来自外部标准。 - NJChim

0

只包含符合规范的代码的简短示例


struct unpacked {  // apparently my other example was too long and confusing
    uint32_t a;    // ...here is a much shorter example with only the conforming
    uint32_t b;    // ...code. (The other program had the gcc-specific declaration,
    uint32_t c;    // but only for test code. Still, it was a bit long.)
    uint32_t troubleMaker;
};

struct unpacked su;
char *bits = "Lorem ipsum dolor";

void f(void) {
  uint32_t x;

  memcpy(&x, bits, 4);
  su.a = x & 7;
  su.b = x >> 3 & 1;
  su.c = x >> 4 & 0x7fff;
  memcpy(&x, bits + 2, 4);
  su.troubleMaker = x >> 3 & 0xffff;
  return 0;
}

请将注释编辑到结构体顶部,就像这样,每行注释都注明了左侧的内容,使得一眼就能理解正在发生的事情。 - Matteo

0

我相信VC++不支持这个,而且我非常怀疑GCC在这方面的行为是否真正符合标准。


1
在这两种情况下都不是标准的。标准没有指定一种打包结构体的方法。MSVC 的 pragma 和 GCC 的 attribute 都是非标准扩展。 - jalf
2
几乎所有关于位域的内容都取决于实现。 - Kragen Javier Sitaker
1
扩展Kragen所说的:因此,两个编译器在这方面都符合标准。问题中声称6字节为“正确”,而8字节为“不正确”的断言违反了标准。 - Steve Jessop

0
如果它绝对需要是6个字节,那么将其定义为3个short并自己获取数据...这不会减慢速度...编译器也只是这样做...

14
如果绝对必须是6个字节,请使用unsigned char[6]。 :) - sellibitze

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