位字段及其对齐如何在C编程中起作用?

我需要你帮助理解位域在C编程中的工作原理。

我已经声明了这个结构:

struct message { unsigned char first_char : 6; unsigned char second_char : 6; unsigned char third_char : 6; unsigned char fourth_char : 6; unsigned char fifth_char : 6; unsigned char sixth_char : 6; unsigned char seventh_char : 6; unsigned char eigth_char : 6; }__packed message; 

我使用sizeof(message)将结构的大小保存为整数。

我认为大小的值将是6,因为6 * 8 = 48位,这是6个字节,但它的大小值为8个字节。

任何人都可以向我解释为什么,以及比特字段和它们的比对是如何工作的?

编辑

我忘了解释我使用结构的情况。 假设我以这种forms接收6字节的void * packetvoid * packet

我然后像这样投射数据:

 message * msg = (message *)packet; 

现在我想打印每个成员的值,所以虽然我将成员声明为6位,但成员使用8位,这会导致打印时出错。 例如,我收到下一个数据:

00001111 11110000 00110011 00001111 00111100 00011100

我认为成员的价值如下所示:

first_char = 000011

second = 111111

third = 000000

fourth = 110011

fifth = 000011

sixth = 110011

seventh = 110000

eigth = 011100

但这不是什么hapening,我希望我解释得很好,如果不是,请告诉我。

位字段不必跨越不同的底层元素(“单元”),因此您可以看到每个字段占用整个unsigned char。 行为是实现定义的,thoug; 比照 C11 6.7.2.1/11:

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

此外,通过6.7.2.1/4中的约束,没有比特字段可能大于适合单个单元的比特字段:

指定位字段宽度的表达式应为整数常量表达式,其非负值不超过将指定的类型的对象的宽度,省略冒号和表达式。

关于位字段的几乎所有内容都是实现定义的。 特别是,如何将位字段打包在一起是实现定义的。 实现不需要让位字段跨越可寻址存储单元的边界,并且看起来您的位置不会。

ISO / IEC 9899:2011§6.7.2.1结构和联合说明符

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

这绝不是位域的“实现定义”特征的结束。

[请选择Kerek SB的答案 ,而不是这个答案 ,因为它有关于§6.7.2.1¶4的重要信息。]


示例代码

 #include  #if !defined(BITFIELD_BASE_TYPE) #define BITFIELD_BASE_TYPE char #endif int main(void) { typedef struct Message { unsigned BITFIELD_BASE_TYPE first_char : 6; unsigned BITFIELD_BASE_TYPE second_char : 6; unsigned BITFIELD_BASE_TYPE third_char : 6; unsigned BITFIELD_BASE_TYPE fourth_char : 6; unsigned BITFIELD_BASE_TYPE fifth_char : 6; unsigned BITFIELD_BASE_TYPE sixth_char : 6; unsigned BITFIELD_BASE_TYPE seventh_char : 6; unsigned BITFIELD_BASE_TYPE eighth_char : 6; } Message; typedef union Bytes_Message { Message m; unsigned char b[sizeof(Message)]; } Bytes_Message; Bytes_Message u; printf("Message size: %zu\n", sizeof(Message)); umfirst_char = 0x3F; umsecond_char = 0x15; umthird_char = 0x2A; umfourth_char = 0x11; umfifth_char = 0x00; umsixth_char = 0x23; umseventh_char = 0x1C; umeighth_char = 0x3A; printf("Bit fields: %.2X %.2X %.2X %.2X %.2X %.2X %.2X %.2X\n", umfirst_char, umsecond_char, umthird_char, umfourth_char, umfifth_char, umsixth_char, umseventh_char, umeighth_char); printf("Bytes: "); for (size_t i = 0; i < sizeof(Message); i++) printf(" %.2X", ub[i]); putchar('\n'); return 0; } 

样本编辑和运行

在Mac OS X 10.9.2 Mavericks上进行GCC 4.9.0测试(64位构建; sizeof(int) == 4sizeof(long_ == 8 )。源代码在bf.c ;创建的程序是bf

 $ gcc -DBITFIELD_BASE_TYPE=char -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf $ ./bf Message size: 8 Bit fields: 3F 15 2A 11 00 23 1C 3A Bytes: 3F 15 2A 11 00 23 1C 3A $ gcc -DBITFIELD_BASE_TYPE=short -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf $ ./bf Message size: 8 Bit fields: 3F 15 2A 11 00 23 1C 3A Bytes: 7F 05 6A 04 C0 08 9C 0E $ gcc -DBITFIELD_BASE_TYPE=int -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf $ ./bf Message size: 8 Bit fields: 3F 15 2A 11 00 23 1C 3A Bytes: 7F A5 46 00 23 A7 03 00 $ gcc -DBITFIELD_BASE_TYPE=long -O3 -g -std=c11 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror bf.c -o bf $ ./bf Message size: 8 Bit fields: 3F 15 2A 11 00 23 1C 3A Bytes: 7F A5 46 C0 C8 E9 00 00 $ 

请注意,4种不同类型的尺寸有4种不同的结果。 另请注意,编译器不需要允许这些类型。 标准说(再次§6.7.2.1):

¶4指定位域宽度的表达式应为整数常量表达式,其非负值不超过指定类型的对象的宽度,冒号和表达式省略。 122)如果该值为零,则声明不应具有声明者。

¶5位字段的类型应为_Boolsigned intunsigned int或其他实现定义类型的限定或非限定版本。

122)虽然_Bool对象中的位数至少为CHAR_BIT ,但_Bool的宽度(符号和值位数)可能只有1位。


另一个子问题

你能解释一下为什么我认为我会得到大小为6的错误吗? 我问了很多朋友,但他们对比特字段知之甚少。

我不确定我对比特字段了解多少。 除了回答有关Stack Overflow的问题之外,我从未使用它们。 在编写便携式软件时,它们没有用处,我的目标是编写便携式软件 - 或者至少是非易失性的软件。

我想你假设一个大致相当于这个位的布局:

 +------+------+------+------+------+------+------+------+ | f1 | f2 | f3 | f4 | f5 | f6 | f7 | f8 | +------+------+------+------+------+------+------+------+ 

它应该代表8位6位的48位,连续布局,没有空格或填充。

现在,不能发生这种情况的一个原因是§6.7.2.1¶4中的规则,当你使用类型T作为位域时,那么位字段的宽度不能大于CHAR_BIT * sizeof(T) 。 在您的代码中, Tunsigned char ,因此位字段不能大于8位,否则它们会跨越存储单元边界。 当然,您的只有6位,但这意味着您无法将第二个位字段放入存储单元。 如果Tunsigned short ,则只有两个6位字段适合16位存储单元; 如果T是32位int ,那么五个6位字段可以适合; 如果T是64位unsigned long ,那么10个6位字段就可以适合。

另一个原因是访问跨越字节边界的这种位字段将是适度低效的。 例如,给定(我的示例代码中定义的Message ):

 Message bf = …initialization code… int nv = 0x2A; bf.second_char = nv; 

假设代码将值视为存储在具有重叠字节边界的字段的压缩字节数组中。 然后代码需要设置下面标记为y的位:

  Byte 0 | Byte 1 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | x | x | x | x | x | x | y | y | y | y | y | y | z | z | z | z | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 

这是一种比特模式。 x位可能对应于first_char ; z位可能对应于third_char一部分; 和y比特到second_char的旧值。 因此,赋值必须复制字节0的前6位,并将新值的2位分配给最后两位:

 ((unsigned char *)&bf)[0] = (((unsigned char *)&bf)[0] & 0xFC) | ((nv >> 4) & 0x03); ((unsigned char *)&bf)[1] = (((unsigned char *)&bf)[1] & 0x0F) | ((nv << 4) & 0xF0); 

如果将其视为16位单元,则代码将等效于:

 ((unsigned short *)&bf)[0] = (((unsigned char *)&bf)[0] & 0xFC0F) | ((nv << 4) & 0x03F0); 

32位或64位分配有点类似于16位版本:

 ((unsigned int *)&bf)[0] = (((unsigned int *)&bf)[0] & 0xFC0FFFFF) | ((nv << 20) & 0x03F00000); ((unsigned long *)&bf)[0] = (((unsigned long *)&bf)[0] & 0xFC0FFFFFFFFFFFFF) | ((nv << 52) & 0x03F0000000000000); 

这对于在比特字段内布置比特的方式做出了一组特定的假设。 不同的假设表达略有不同,但如果将位字段视为连续的位数组,则需要类似于此的假设。

相比之下,实际使用的每字节布局为6位,分配变得更加简单:

 ((unsigned char *)&bf)[1] = nv & 0x3F; 

编译器省略掩码操作是合理的,因为填充位中的值是不确定的(但值必须是8位赋值)。

访问位字段所需的代码量是大多数人避免使用它们的一个原因。 不同的编译器可以针对相同的定义做出不同的布局假设,这意味着不能在不同类型的机器之间可靠地传递值。 通常,ABI将定义标准C不具备的细节,但如果一台机器是PowerPC或SPARC而另一台机器是基于英特尔,则所有投注都将关闭。 自己做变换和掩饰变得更好; 至少计算的成本是可见的。