char 和 int 上的 C struct 位字段之间的区别

different between C struct bitfields on char and on int

在 C 中使用位域时,我发现了与用于声明字段的实际类型有关的差异,这是我没有预料到的。

我没有找到任何明确的解释。现在,问题已确定,因此尽管没有明确的答复,但此 post 可能对面临相同问题的任何人都有用。 不过,如果有人能给出正式的解释,那就太好了。

下面的结构,占用内存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;

这个占内存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;

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

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

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

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

在这两种情况下,

因此,在这两种情况下,有用的数据都需要 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;
}

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

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

  1. An implementation may allocate any addressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.

C11/C17 6.7.2.1p5

有关
  1. A. bit-field shall have a type that is a qualified or unqualified version of _Bool, signed int, unsigned int, or some other implementation-defined type. It is implementation-defined whether atomic types are permitted.

并且您正在使用 char 意味着一般没有什么可讨论的 - 对于特定的实现,请查看编译器手册。 Here's the one for GCC.

从 2 个摘录中可以看出,实现可以自由使用 任何它想要的类型 来实现位域 - 它甚至可以对两者使用 int64_t这些情况的结构大小为 16 字节。符合规范的实现 必须做的唯一一件事 是在剩余 space 足够的情况下将位打包到所选的可寻址存储单元中。


对于 System-V ABI on 386-compatible (32-bit processors) 上的 GCC,以下代表:

Plain bit-fields (that is, those neither signed nor unsigned) always have non- negative values. Although they may have type char, short, int, long, (which can have negative values), these bit-fields have the same range as a bit-field of the same size with the corresponding unsigned type. Bit-fields obey the same size and alignment rules as other structure and union members, with the following additions:

  • Bit-fields are allocated from right to left (least to most significant).
  • A bit-field must entirely reside in a storage unit appropriate for its declared type. Thus a bit-field never crosses its unit boundary.

  • Bit-fields may share a storage unit with other struct/union members, including members that are not bit-fields. Of course, struct members occupy different parts of the storage unit.

  • Unnamed bit-fields' types do not affect the alignment of a structure or union, although individual bit-fields' member offsets obey the alignment constraints.

即在 System-V ABI 中,386,int f: 1 表示位域 f 必须在 int。如果 space 的整个字节仍然存在,同一结构中的后续 char 将被 打包在此 int 中,即使它不是一个位-field.

利用这些知识,

的布局
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;

将会

                     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 ---------------------------->

的布局
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;

将会

                    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 ---->

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

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

struct y {
    int a : 2;  
    int b : 15;
    char  c ; 
};

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


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