我书中的位字段结构的大小不准确

时间:2018-04-26 10:53:13

标签: c gcc struct field bit

我正在研究C语言的基础知识。我用位字段到达了结构章节。本书展示了一个包含两种不同类型数据的结构示例:各种bool和各种无符号整数。

本书声明该结构的大小为16位,如果不使用填充,该结构将测量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

这是我得到的结果:

enter image description here

以下是该示例所在页面的图片:

enter image description here

7 个答案:

答案 0 :(得分:28)

Microsoft ABI以与GCC通常在其他平台上执行的方式不同的方式布局位域。您可以选择将Microsoft兼容布局与-mms-bitfields选项一起使用,也可以使用-mno-ms-bitfields禁用它。您的GCC版本可能默认使用-mms-bitfields

根据文档,启用-mms-bitfields时:

  
      
  • 每个数据对象都有对齐要求。除结构,联合和数组之外的所有数据的对齐要求是对象的大小或当前打包大小(使用aligned属性或pack pragma指定),以较小者为准。对于结构,联合和数组,对齐要求是其成员的最大对齐要求。为每个对象分配一个偏移量,以便:   offset%alignment_requirement == 0
  •   
  • 如果整数类型具有相同的大小并且下一个位字段适合当前分配单元而不跨越边界,则相邻的位字段被打包到相同的1字节,2字节或4字节分配单元中通过位域的通用对齐要求。
  •   

由于boolunsigned int具有不同的大小,因此它们被单独打包和对齐,这大大增加了结构大小。 unsigned int的对齐是4个字节,并且必须在结构中间重新对齐三次导致总大小为16字节。

通过将bool更改为unsigned int或指定-mno-ms-bitfields,您可以获得相同的书籍行为(但这意味着您无法与代码互操作在Microsoft编译器上编译。)

请注意,C标准未指定如何布置位域。所以你的书所说的对于某些平台可能是正确的,但对所有平台都不是。

答案 1 :(得分:8)

当描述C语言标准的规定时,您的文本会提出不合理的主张。具体来说,标准不仅说unsigned int是任何类型结构的基本布局单元,它明确否认对存储单元大小的任何要求,其中位域表示是存储:

  

实现可以分配任何可寻址的存储单元   足以容纳一个位域。

C2011, 6.7.2.1/11

该文本还假设标准不支持填充。 C实现可以在struct的任何或所有元素和位域存储单元之后包括任意数量的填充,包括最后一个。实现通常使用此自由来解决数据对齐问题,但C不限制填充到该用途。这与您的文本引用的未命名位域完全分开&#34;填充&#34;。

我想这本书应该受到赞扬,但是,为了避免令人不安的常见误解,即C要求声明的位字段数据类型与其表示所在的存储单元的大小有关。 。该标准没有这种关联。

  

为什么在我的编译器上使用填充完全相同的结构测量16个字节(而不是位)而没有填充的16个字节?

为尽可能多地减少文本,它确实区分成员占用的数据位数(总共16位,6个属于未命名的位域)和struct实例的总大小。似乎断言整体结构将是unsigned int的大小,在它描述的系统上显然是32位,并且对于两个版本的结构都是相同的。

原则上,您的观察尺寸可以通过使用128位存储单元进行位域实现来解释。在实践中,它可能使用一个或多个较小的存储单元,因此每个结构中的一些额外空间可归因于实现提供的填充,例如我在上面触摸。

C实现对所有结构类型施加最小大小是很常见的,因此在必要时将表示填充到该大小。通常,这个大小符合系统支持的任何数据类型的最严格的对齐要求,尽管这又是一个实现考虑因素,而不是语言的要求。

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

答案 2 :(得分: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

BUT

来自theonlinecompiler.com的

GCC 4.9.2:

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

(为了正确编译,您需要将%zu发送到%u

修改

@ interjay的answer可能会解释这一点。当我从WandBox向GCC 4.9.2添加-mms-bitfields时,我得到了这个:

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

答案 3 :(得分: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个字节,用于之间和之后的字段。

答案 4 :(得分:5)

在作者定义结构之前,他说他想将位字段分成两个字节,因此将有一个字节包含填充相关信息的位域和一个字节用于边界相关信息。

为实现这一点,他添加(插入)一些未使用的位(位域):

unsigned int       4;  // padding of the first byte

他也填充了第二个字节,但没有必要。

因此,在填充之前,将使用10位,并且在填充之后,定义了16位(但是没有使用它们)。

<小时/> 注意:作者使用bool表示1/0字段。作者接下来假设_Bool C99类型别名为bool。但似乎编译器在这里有点困惑。用bool替换unsigned int可以解决问题。来自C99:

  

6.3.2:无论int还是unsigned int都可以在表达式中使用以下内容   使用:

     
      
  • 类型为_Boolintsigned intunsigned int的位字段。
  •   

答案 5 :(得分:2)

你完全误解了这本书的内容。

声明了16位的位字段。 6位是未命名的字段,不能用于任何内容 - 这是提到的填充。 16位减6位等于10位。不计算填充字段,结构有10个有用的位。

struct的字节数取决于编译器的质量。显然你遇到了一个没有在一个结构中打包bool位域的编译器,它使用4个字节用于bool,一些内存用于位域,加上struct padding,总共4个字节,另外4个字节用于bool,更多内存对于位域,加上struct padding,总共4个字节,最多可加16个字节。真的很难过。这个结构可能相当合理地是两个字节。

答案 6 :(得分:1)

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

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

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

  3. 我认为#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放置为可以支持其“bunkmate”的字对齐访问的方式f2。如果代码是:

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

    然后f1x放在f2后面,f1放在一个单词中。

    请注意,C89标准添加了一种语法来强制布局,以防止在使用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; }; 放在一个字节中:

    {{1}}

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