在我的代码中,位域结构体的大小不准确

25

我正在学习C语言的基础知识,现在来到了位域结构体章节。书中展示了一个包含不同类型数据的结构体例子:多种布尔类型和多种无符号整数。

书中声明该结构体大小为16位,而如果不使用填充(padding),该结构体大小将只有10位。

这是书中例子所使用的结构体:

#include <stdio.h>
#include <stdbool.h>

struct test{

       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
};

int main(void)
{
       struct test Test;

       printf("%zu\n", sizeof(Test));

       return 0;
}

为什么在我的编译器上,相同的结构在加上填充后占用16个字节(而不是位),没有填充则占用16个字节?

我正在使用

GCC (tdm-1) 4.9.2 compiler;
Code::Blocks as IDE.
Windows 7 64 Bit
Intel CPU 64 bit

这是我得到的结果:

输入图片描述

这是一个例子所在页面的图片:

输入图片描述


1
评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
4
据我记得,GCC 在这里存在一些已知的bug。话虽如此,请勿使用位域(bit-fields)。不要读关于使用位域的书。 - Lundin
3
这本书看起来像垃圾。C语言并不是“使用无符号整数作为具有位域的结构体的基本布局单元”。根据C标准中的《6.7.2.1 结构和联合说明符》第5段,位域可以是任何类型:“位域必须具有限定或未限定版本的 _Boolsigned intunsigned int 或其他一些实现定义的类型。原子类型是否允许是实现定义的。” - Andrew Henle
7个回答

30
Microsoft ABI对位域的布局方式与GCC在其他平台上通常采用的方式不同。您可以选择使用兼容Microsoft的布局,通过-mms-bitfields选项,或者通过-mno-ms-bitfields禁用它。您的GCC版本很可能默认使用-mms-bitfields
根据文档,当启用-mms-bitfields时:
  • 每个数据对象都有一个对齐要求。除了结构、联合和数组之外的所有数据的对齐要求都是对象的大小或当前打包大小(用对齐属性或pack pragma指定),以较小值为准。对于结构、联合和数组,对齐要求是其成员的最大对齐要求。为了满足: offset % alignment_requirement == 0
  • 如果整数类型的大小相同,并且下一个位域适合于当前分配单元而不会越过由位域的公共对齐要求所强制实施的边界,则相邻的位域将被打包到相同的1、2或4字节分配单元中。
由于boolunsigned int的大小不同,它们被分开打包和对齐,这会显著增加结构体的大小。 unsigned int的对齐要求为4字节,在结构体的中间必须重新对齐三次,导致总大小为16字节。
您可以通过将bool更改为unsigned int或指定-mno-ms-bitfields来获得与书相同的行为(尽管这意味着您不能与使用Microsoft编译器编译的代码互操作)。
请注意,C标准没有指定位域如何布局。因此,您的书所说的可能适用于某些平台,但并非所有平台都是这样。

22
总的来说,这是一个很棒的回答,但最后一段才是真正的关键点;太多的书籍在没有任何限定的情况下陈述实现细节,这让我感到烦恼... - Lightness Races in Orbit
在最后一句中,你可以将“标准”改为“C语言标准”,因为正如你所指出的,确实存在这样的标准。 - Owen
2
尽管C标准没有规定位域的布局细节,但它确实放置了一些要求(参见[C11 6.7.2.1/11](http://port70.net/~nsz/c/c11/n1570.html#6.7.2.1p11))。当然还有很大的灵活性可供利用,但就OP的结构而言,MS ABI需要一种布局,不能被解释为符合C标准。微软的不一致又来了! - John Bollinger

8
描述C语言标准规定的内容,你的文本提出了不合理的要求。具体来说,标准不仅没有说unsigned int是任何类型结构的基本布局单元,而且明确否认对存储位域表示的存储单元大小的任何要求:
“实现可以分配任何可寻址的存储单元,足够容纳一个位域。”
(C2011, 6.7.2.1/11)
此外,该文本对填充的假设也得不到标准的支持。C实现可以在任何或所有元素和struct的位域存储单元之后包含任意数量的填充,包括最后一个。实现通常使用这种自由来解决数据对齐考虑,但C并不限制填充的使用。这与你的文本所指的未命名位域完全无关。
我认为这本书应该受到赞扬,因为它避免了一个令人沮丧的常见误解,即 C 要求位域的声明数据类型与其表示所在的存储单元的大小有关。标准没有这样的要求。
“为什么我的编译器上相同的结构在填充后测量出 16 字节(而不是位),而在没有填充时是 16 字节?”
为了尽可能地给文本留下余地,它确实区分了成员占用的数据位数(总共 16 位,其中 6 位属于未命名的位域)和结构实例的整体大小。它似乎在断言整个结构将是一个“unsigned int”的大小,显然在描述该系统的系统上是 32 位,并且对于结构的两个版本来说都是相同的。
原则上,你观察到的大小可以通过你的实现使用 128 位存储单元来解释位域。实际上,它可能使用一个或多个较小的存储单元,因此每个结构中的一些额外空间归因于实现提供的填充,就像我上面提到的那样。

在C语言实现中,对于所有结构类型都会有一个最小尺寸的限制,并且必要时会对表示进行填充以达到这个尺寸。通常,这个尺寸与系统支持的任何数据类型的最严格对齐要求相匹配,但这又是实现考虑而非语言要求。

总之,只有依靠实现细节和/或扩展,才能预测一个struct的确切大小,无论是否存在位域成员。


6

令我惊讶的是,一些GCC 4.9.2在线编译器之间似乎存在差异。首先,这是我的代码:

#include <stdio.h>
#include <stdbool.h>

struct test {
       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
};

struct test_packed {
       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
} __attribute__((packed));

int main(void)
{
       struct test padding;
       struct test_packed no_padding;

       printf("with padding: %zu bytes = %zu bits\n", sizeof(padding), sizeof(padding) * 8);
       printf("without padding: %zu bytes = %zu bits\n", sizeof(no_padding), sizeof(no_padding) * 8);

       return 0;
}

现在,不同编译器的结果。

来自WandBox的GCC 4.9.2:

with padding: 4 bytes = 32 bits
without padding: 2 bytes = 16 bits

来自http://cpp.sh/的GCC 4.9.2:

with padding: 4 bytes = 32 bits
without padding: 2 bytes = 16 bits

但是

来自theonlinecompiler.com的GCC 4.9.2:

with padding: 16 bytes = 128 bits
without padding: 16 bytes = 128 bits

为了正确编译,您需要将%zu更改为%u

编辑

@interjay的答案可能会解释这个问题。当我向WandBox安装了GCC 4.9.2加上-mms-bitfields之后,我得到了如下输出:

with padding: 16 bytes = 128 bits
without padding: 16 bytes = 128 bits

2
好的答案 - UV。细节: “似乎有问题”不是一个问题,而是一个令人惊讶的差异。结果为16是一个合规的答案 - 并且在目标机器上可能更加高效。 - chux - Reinstate Monica
@chux,谢谢,你说得对,我已经纠正了我的答案。 - Grzegorz Adam Kowalski
2
看起来theonlinecompiler.com在Windows上运行GCC,这就是区别所在。 - interjay
@interjay,我相信你是对的。我编辑了我的答案并引用了你的参考。 - Grzegorz Adam Kowalski

6

标准C语言并没有描述变量在内存中应该如何排列的所有细节。这为依赖于所使用平台的优化留下了余地。

如果您想了解有关内存中元素的位置如何安排的信息,您可以按照以下方式进行:

#include <stdio.h>
#include <stdbool.h>

struct test{

       bool opaque                 : 1;
       unsigned int fill_color     : 3;
       unsigned int                : 4;
       bool show_border            : 1;
       unsigned int border_color   : 3;
       unsigned int border_style   : 2;
       unsigned int                : 2;
};


int main(void)
{
  struct test Test = {0};
  int i;
  printf("%zu\n", sizeof(Test));

  unsigned char* p;
  p = (unsigned char*)&Test;
  for(i=0; i<sizeof(Test); ++i)
  {
    printf("%02x", *p);
    ++p;
  }
  printf("\n");

  Test.opaque = true;

  p = (unsigned char*)&Test;
  for(i=0; i<sizeof(Test); ++i)
  {
    printf("%02x", *p);
    ++p;
  }
  printf("\n");

  Test.fill_color = 3;

  p = (unsigned char*)&Test;
  for(i=0; i<sizeof(Test); ++i)
  {
    printf("%02x", *p);
    ++p;
  }
  printf("\n");

  return 0;
}

在 ideone 上运行此代码(https://ideone.com/wbR5tI),我得到以下结果:
4
00000000
01000000
07000000

我可以看到opaquefill_color都在第一个字节中。 在Windows机器上运行完全相同的代码(使用gcc)会得到:

16
00000000000000000000000000000000
01000000000000000000000000000000
01000000030000000000000000000000

所以我可以看到,opaquefill_color并不都在第一个字节中。看起来opaque占用了4个字节。
这解释了你总共得到16个字节,即每个bool占用4个字节,然后是中间和之后的字段占用4个字节。

为避免未定义的行为,您的格式需要与参数类型匹配。在这种情况下,您需要将unsigned char与形式为%02hhx(请注意“hh”)的指令匹配。看起来观察到的行为可能是您想要的,但不要依赖运气。 - John Bollinger
请注意,尽管 C 语言不要求 opaquefill_color 成员出现在同一个字节中,但它确实要求它们出现在同一个“可寻址存储单元”中,无论实现如何选择,只要有足够的空间即可。如果分配给 opaque 的存储单元大于四个字节或小于 4 位,则观察到的 16 字节布局符合要求。 - John Bollinger

5
在作者定义结构体之前,他提到他想将位域分成两个字节,因此将有一个字节包含与填充相关的位域信息,另一个字节包含与边框相关的位域信息。
为了实现这一点,他添加(插入)了一些未使用的位(位域):
unsigned int       4;  // padding of the first byte

他还填充了第二个字节,但其实这是不需要的。
因此,在填充之前,有10个比特位在使用中,填充后定义了16个比特位(但并非全部都在使用)。
注意:作者使用bool表示1/0字段。作者接下来假设_BoolC99类型与bool别名。但似乎编译器在这里有些混淆。将bool替换为unsigned int就可以解决这个问题。来自C99:

6.3.2:以下内容可在表达式中使用,任何可以使用int或unsigned int的地方:

  • 类型为_Boolintsigned intunsigned int的位域。

1
@interjay,确实如此。在我看来,他混淆了位和字节。 - Paul Ogilvie
1
如果 boolunsigned 导致编译器对这些字段进行对齐,那么 16 字节的结果并不奇怪。 - chux - Reinstate Monica
1
@chux 我猜这是可能的,尽管在位域中将字段对齐到中间是非常奇怪的行为。特别是当它在Linux上没有复现时。也许这是GCC在Windows上执行的一些操作,以遵循平台ABI的规定? - interjay
1
@joop:Bool 位域的语义与单个位 unsigned char 位域的语义不同。将值 2 存储到任何非 Bool 无符号类型的单个位字段中将清除该位,而将值 2 存储到单个位 Bool 中将设置该位。另一方面,我认为任何试图向单个位字段写入除 0 或 1 以外的任何内容的代码都应被视为高度可疑。如果 x 可能具有除 0 或 1 以外的值,则希望将其存储到位域的代码应编写为 thing.field = !!(x)thing.field = (x & 1); 以明确... - supercat
1
尽管如此,如果x具有其他值,仍需考虑预期的行为。由于标准规定foo.singleBitNonBool=x;等同于foo.singleBitNonBool=(x & 1);foo.singleBitBool=x;等同于foo.singleBitBool=!!(x);,因此仅使用x的代码将受到语义差异的影响。 - supercat
显示剩余14条评论

2
你完全误解了这本书所说的内容。
声明了16位的位域,其中6位是未命名的字段,不能用于任何用途 - 这就是提到的填充。 16位减去6位等于10位。不计算填充字段,该结构具有10个有用位。
该结构体占用多少字节取决于编译器的质量。 显然,您遇到了一个不会将bool位域打包在结构体中的编译器,并且它为bool使用4个字节,一些内存用于位域,加上结构填充,总共4个字节,再加上4个字节的bool,更多的位域内存,加上结构填充,总共4个字节,总共16个字节。真是太可悲了。这个结构体可以合理地缩小为两个字节。

1

历史上有两种常见的位域元素类型解释方式:

  1. 检查类型是否为有符号或无符号,但在决定元素应放置在哪里时忽略“char”、“short”、“int”等之间的区别。

  2. 除非一个位域前面有与其相同类型或对应的有符号/无符号类型的另一个位域,否则分配该类型的对象并将位域放置其中。如果它们适合,则将具有相同类型的后续位域放入该对象中。

我认为#2背后的动机是,在需要字对齐16位值的平台上,编译器会给出以下内容:

struct foo {
  char x;  // Not a bitfield
  someType f1 : 1;
  someType f2 : 7;
};

也许可以分配一个两字节的结构体,将两个字段都放在第二个字节中,但如果这个结构体是这样的:

struct foo {
  char x;  // Not a bitfield
  someType f1 : 1;
  someType f2 : 15;
};

需要所有的f2都适合于一个16位字中,这将需要在f1之前添加填充字节。由于公共初始序列规则,f1必须在这两个结构体中放置相同,这意味着如果f1满足了公共初始序列规则,即使在第一个结构体中,它也需要在其之前添加填充。

目前,希望在第一种情况下允许更密集布局的代码可以这样写:

struct foo {
  char x;  // Not a bitfield
  unsigned char f1 : 1;
  unsigned char f2 : 7;
};

并邀请编译器将这两个位域放入紧随x之后的字节中。由于类型被指定为unsigned char,编译器不需要担心15位字段的可能性。如果布局如下:

struct foo {
  char x;  // Not a bitfield
  unsigned short f1 : 1;
  unsigned short f2 : 7;
};

原意是让 f1f2 存储在同一个位置,那么编译器需要将 f1 放置在可以支持其“室友” f2 进行字对齐访问的位置。如果代码如下:

struct foo {
  char x;  // Not a bitfield
  unsigned char f1 : 1;
  unsigned short f2 : 15;
};

如果这样,f1 将会跟在 x 之后,而 f2 则会独立成词。
请注意,C89 标准添加了一种语法来强制布局,以防止 f1 被放置在使用存储的 f2 前一个字节中。
struct foo {
  char x;  // Not a bitfield
  unsigned short : 0;  // Forces next bitfield to start at a "short" boundary
  unsigned short f1 : 1;
  unsigned short f2 : 15;
};

在C89中,添加了:0语法,这在很大程度上消除了编译器将更改类型视为强制对齐的需要,除非处理旧代码。

1
C89、C99和C11都表示:“如果在同一可寻址存储单元中有足够的空间,则立即跟随结构体中另一个位域的位域应打包到同一单元的相邻位中。” 我没有看到任何合理的解释“可寻址存储单元”,可以符合方法(2)的要求。 - John Bollinger
@JohnBollinger:我认为OP看到的行为是编译器应用原则#2的结果。根据实验,clang和gcc似乎有一些有趣和古怪的规则,它们查看后面字段的类型来决定它是否“适合”与前一个字段存储在同一位置,但我不知道确切的规则。 - supercat
观察到的行为可能确实是由编译器应用原则#2导致的,但是只要这种行为是不符合规范的,我就对它有多普遍感兴趣。而且它确实是不符合规范的。标准允许实现在选择多大ASU时有很大的自由度,但是它的规范是否将位字段分配给相同或不同的字段没有留下根据声明类型进行区分的余地。 - John Bollinger
我只能谈论C语言的语义以及观察到的布局和行为如何匹配。根据C语言,仅用于存储位域成员的一个或多个ASU是单独分配的,与常规结构成员的存储不同。这些的大小和对齐方式未指定,但对位域在其中布局放置了一些要求。然而,gccclang实际上到达了您描述的布局(我可以在此处复制),它可以用我已经介绍的符合方式来表征。 - John Bollinger
@JohnBollinger:标准对编译器应在哪种可寻址存储单元中寻找空间的规定相当模糊。无论如何,就原始问题而言,真正重要的是编译器的实际行为。顺便说一句,标准并不要求实现决策与实际CPU架构有任何关系。 - supercat
显示剩余14条评论

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