几乎所有关于位域的内容都是由实现定义的。特别是,如何将位字段打包在一起是实现定义的。实现不需要让位域跨越可寻址存储单元的边界,而您的实现似乎没有。
ISO/IEC 9899:2011 §6.7.2.1 结构和联合说明符
¶11 实现可以分配任何大到足以容纳位字段的可寻址存储单元。
如果有足够的空间,一个位域紧跟在另一个位域之后
结构应打包到同一单元的相邻位中。如果剩余空间不足,
不适合的位域是否放入下一个单元或与相邻单元重叠是
实现定义。一个单元内位域的分配顺序(高位到
低阶或低阶到高阶)是实现定义的。对齐方式
未指定可寻址存储单元
这绝不是位域的“实现定义”特性的终结。
[请选择Kerek SB 的answer 而不是这个,因为它也包含有关§6.7.2.1 ¶4 的重要信息。]
示例代码
#include <stdio.h>
#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));
u.m.first_char = 0x3F;
u.m.second_char = 0x15;
u.m.third_char = 0x2A;
u.m.fourth_char = 0x11;
u.m.fifth_char = 0x00;
u.m.sixth_char = 0x23;
u.m.seventh_char = 0x1C;
u.m.eighth_char = 0x3A;
printf("Bit fields: %.2X %.2X %.2X %.2X %.2X %.2X %.2X %.2X\n",
u.m.first_char, u.m.second_char, u.m.third_char,
u.m.fourth_char, u.m.fifth_char, u.m.sixth_char,
u.m.seventh_char, u.m.eighth_char);
printf("Bytes: ");
for (size_t i = 0; i < sizeof(Message); i++)
printf(" %.2X", u.b[i]);
putchar('\n');
return 0;
}
示例编译和运行
使用 GCC 4.9.0(64 位构建;sizeof(int) == 4 和 sizeof(long_ == 8)在 Mac OS X 10.9.2 Mavericks 上进行测试。源代码在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 位字段的类型应为 _Bool、signed
int、unsigned 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)。在您的代码中,T 是 unsigned char,因此位域不能大于 8 位,否则它们会跨越存储单元边界。当然,您的只有 6 位,但这意味着您无法将第二个位域放入存储单元。如果T 是unsigned 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 的旧值。因此,赋值必须复制 Byte 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,而另一台基于 Intel,那么所有的赌注都没有了。做转移和掩饰自己会变得更好;至少计算成本是可见的。