char 和 int 上的 C 结构位域之间的区别

Fra*_*ois 4 c struct sizeof bit bit-fields

当在 C 中使用位字段时,我发现了与用于声明字段的实际类型相关的意想不到的差异。

我没有找到任何明确的解释。现在,问题已经确定,因此,尽管没有明确的回应,但这篇文章可能对面临同样问题的任何人都有用。不过,如果有人能给出正式的解释,那就太好了。

以下结构在内存中占用 2 个字节。

struct {
  char field0 : 1; // 1 bit  - bit 0 
  char field1 : 2; // 2 bits - bits 2 down to 1
  char field2 ;    // 8 bits - bits 15 down to 8
} reg0;
Run Code Online (Sandbox Code Playgroud)

这个占用内存4个字节,问题是为什么?

struct {
  int  field0 : 1; // 1 bit  - bit 0 
  int  field1 : 2; // 2 bits - bits 2 down to 1
  char field2 ;    // 8 bits - bits 15 down to 8
} reg1;
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,这些位在内存中的组织方式相同:字段 2 始终将位 15 降低到位 8。

我试图找到一些关于这个主题的文献,但仍然无法得到明确的解释。

我能找到的两个最合适的链接是:

然而,没有人真正解释为什么第二个结构占用 4 个字节。实际上仔细阅读,我什至期望该结构占用 2 个字节。

在这两种情况下,

  • field0 占用 1 位
  • field1 占用 2 位
  • field2 占用 8 位,并与第一个可用字节地址对齐

因此,在这两种情况下,有用数据都需要 2 个字节。

那么,幕后是什么让 reg1 占用 4 个字节呢?

完整代码示例:

#include "stdio.h"
// Register Structure using char
typedef struct {
    // Reg0
    struct _reg0_bitfieldsA {
      char field0 : 1;
      char field1 : 2;
      char field2 ;
    } reg0;

    // Nextreg
    char NextReg;

} regfileA_t;

// Register Structure using int
typedef struct {
    // Reg1
    struct  _reg1_bitfieldsB {
      int field0 : 1;
      int field1 : 2;
      char field2 ;
    } reg1;

    // Reg
    char NextReg;
} regfileB_t;


regfileA_t regsA;
regfileB_t regsB;


int main(int argc, char const *argv[])
{
    int* ptrA, *ptrB;

    printf("sizeof(regsA) == %-0d\n",sizeof(regsA));   // prints 3 - as expected
    printf("sizeof(regsB) == %-0d\n",sizeof(regsB));   // prints 8 - why ?
    printf("\n");
    printf("sizeof(regsA.reg0) == %-0d\n",sizeof(regsA.reg0)); // prints 2 - as epxected
    printf("sizeof(regsB.reg0) == %-0d\n",sizeof(regsB.reg1)); // prints 4 - int bit fields tells the struct to use 4 bytes then.
    printf("\n");
    printf("addrof(regsA.reg0) == 0x%08x\n",(int)(&regsA.reg0));     // 0x0804A028
    printf("addrof(regsA.reg1) == 0x%08x\n",(int)(&regsA.NextReg));  // 0x0804A02A = prev + 2
    printf("addrof(regsB.reg0) == 0x%08x\n",(int)(&regsB.reg1));     // 0x0804A020
    printf("addrof(regsB.reg1) == 0x%08x\n",(int)(&regsB.NextReg));  // 0x0804A024 = prev + 4 - my register is not at the righ place then.
    printf("\n");

    regsA.reg0.field0 = 1;
    regsA.reg0.field1 = 3;
    regsA.reg0.field2 = 0xAB;

    regsB.reg1.field0 = 1;
    regsB.reg1.field1 = 3;
    regsB.reg1.field2 = 0xAB;

    ptrA = (int*)&regsA;
    ptrB = (int*)&regsB;
    printf("regsA.reg0.value == 0x%08x\n",(int)(*ptrA)); // 0x0000AB07 (expected)
    printf("regsB.reg0.value == 0x%08x\n",(int)(*ptrB)); // 0x0000AB07 (expected)

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

当我第一次编写结构体时,我希望 reg1 仅占用 2 个字节,因此下一个寄存器的偏移量 = 2。

Ant*_*ala 8

标准的相关部分是C11/C17 6.7.2.1p11

  1. 实现可以分配足够大的任何可寻址存储单元来容纳位字段。如果剩余足够的空间,则结构中紧跟在另一个位字段之后的位字段应被打包到同一单元的相邻位中。如果剩余空间不足,则不适合的位字段是否放入下一个单元或与相邻单元重叠是实现定义的。单元内位字段的分配顺序(高位到低位或低位到高位)是由实现定义的。可寻址存储单元的对齐方式未指定。

与C11/C17 6.7.2.1p5相关

  1. A. 位字段的类型应为 _Bool、signed int、unsigned int 或其他一些实现定义的类型的限定或非限定版本。是否允许原子类型是由实现定义的。

并且您正在使用的char意味着一般没有什么可讨论的 - 对于特定的实现,请检查编译器手册。这是 GCC 的

从这两个摘录可以看出,实现绝对可以自由地使用它想要实现位字段的任何类型 - 它甚至可以用于int64_t具有大小为 16 字节的结构的这两种情况。一致的实现必须做的唯一一件事是,如果还有足够的空间,则将这些位打包到所选的可寻址存储单元中。


对于386 兼容(32 位处理器)上的 System-V ABI上的 GCC ,以下内容表示:

普通位字段(即既不是signed也不是 的位字段unsigned)始终具有非负值。尽管它们的类型可能为char, short, int, long, (可以有负值),但这些位域与具有相应大小的位域具有相同的范围。unsigned。位域遵循与其他结构和联合成员相同的大小和对齐规则,并添加了以下内容:

  • 位字段从右到左(最低有效位到最高有效位)分配。
  • 位字段必须完全驻留在适合其声明类型的存储单元中。因此,位域永远不会跨越其单元边界。

  • 位字段可以与其他struct/union成员共享存储单元,包括不是位字段的成员。当然, struct成员占据存储单元的不同部分。

  • 尽管各个位域的成员偏移量遵守对齐约束,但未命名位域的类型不会影响结构或联合的对齐。

即在 System-V ABI 中,386int f: 1表示位字段f必须位于int. char如果剩余整个字节的空间,则同一结构中的后续内容将被打包在此内部int,即使它不是位字段。

利用这些知识,可以进行布局

struct {
  int  a : 1; // 1 bit  - bit 0 
  int  b : 2; // 2 bits - bits 2 down to 1
  char c ;    // 8 bits - bits 15 down to 8
} reg1;
Run Code Online (Sandbox Code Playgroud)

                     1                     2                 3  
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
|a b b x x x x x|c c c c c c c c|x x x x x x x x|x x x x x x x x|               

<------------------------------ int ---------------------------->
Run Code Online (Sandbox Code Playgroud)

和布局

struct {
  char  a : 1; // 1 bit  - bit 0 
  char b : 2; // 2 bits - bits 2 down to 1
  char c ;    // 8 bits - bits 15 down to 8
} reg1;
Run Code Online (Sandbox Code Playgroud)

                    1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 
|a b b x x x x x|c c c c c c c c|

<---- char ----><---- char ---->
Run Code Online (Sandbox Code Playgroud)

因此存在棘手的边缘情况。比较这里的 2 个定义:

struct x {
    short a : 2;  
    short b : 15;
    char  c ; 
};

struct y {
    int a : 2;  
    int b : 15;
    char  c ; 
};
Run Code Online (Sandbox Code Playgroud)

因为位域不能跨越单元边界,所以struct x成员ab需要走不同的短线。那么就没有足够的空间来容纳char c,所以它必须在那之后。并且整个结构必须适当对齐,short因此在 i386 上它将是 6 个字节。然而,后者将把a和打包b在 的 17 个最低位中int,并且由于 中仍然剩下一个完整的可寻址字节int,因此c也将打包在这里,因此sizeof (struct y)将是4


最后,您必须真正指定 或 是否intchar签名 - 默认值可能不是您所期望的!标准将其留给实现,而 GCC 有一个编译时开关来更改它们。