C中的位域内存管理

6
为了理解位域内存存储,我创建了下面的测试程序。
#include <stdio.h>

int main()
{
    int a;
    typedef struct {
        int b7 : 1;
        int b6 : 1;
        int b5 : 1;
        int b4 : 1;
        int b3 : 1;
        int b2 : 1;
        int b1 : 1;
        int b0 : 1;
    } byte;

    byte ab0 = {0,0,0,0,0,0,0,1};
    a = *(int*)&ab0;
    printf("ab0 is %x \n",a);

    byte ab1 = {0,0,0,0,0,0,1,0};
    a = *(int*)&ab1;
    printf("ab1 is %x \n",a);

    byte ab2 = {0,0,0,0,0,1,0,0};
    a = *(int*)&ab2;
    printf("ab2 is %x \n",a);

    byte ab3 = {0,0,0,0,1,0,0,0};
    a = *(int*)&ab3;
    printf("ab3 is %x \n",a);

    byte ab4 = {0,0,0,1,0,0,0,0};
    a = *(int*)&ab4;
    printf("ab4 is %x \n",a);

    byte ab5 = {0,0,1,0,0,0,0,0};
    a = *(int*)&ab5;
    printf("ab5 is %x \n",a);

    byte ab6 = {0,1,0,0,0,0,0,0};
    a = *(int*)&ab6;
    printf("ab6 is %x \n",a);

    byte ab7 = {1,0,0,0,0,0,0,0};
    a = *(int*)&ab7;
    printf("ab7 is %x \n",a);

    return 0;
}

编译和运行
gcc -Wall test.c
./a.out 
ab0 is 80 
ab1 is 40 
ab2 is 20 
ab3 is 10 
ab4 is 8 
ab5 is 4 
ab6 is 2 
ab7 is 1 

在线运行代码时输出相同 http://codepad.org/ntqyuixp

我无法理解它的输出。

期望输出: 根据我的理解,输出应该是这样的:

ab0 is 1 
ab1 is 2 
ab2 is 4 
ab3 is 8 
ab4 is 10 
ab5 is 20 
ab6 is 40 
ab7 is 80
  • 请告诉我我漏掉了什么。

  • 字节序是否起到任何作用?

  • 代码应该如何编写以实现我的预期行为?


1
我知道你只是为了实验而这样做,但是将类型转换为int*然后解引用的方法是错误的,如果你在生产代码中应用它,会遇到很多麻烦。不要这样做。标准预见的执行此类检查的方法是使用union - Jens Gustedt
3个回答

10

位域中位的顺序是由实现定义的。实现与您预期的定义不同,这几乎就是关于它的全部内容。

有关位域的几乎所有内容都是由实现定义的。

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

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

¶5 位域的类型应为限定或未限定版本的 _Boolsigned intunsigned int 或其他某些实现定义类型。
原子类型是否允许则由实现定义。

¶9 结构体或联合体的成员可为任何完整对象类型(变长类型除外)。123) 此外,成员可以被声明为由指定数量的位组成(包括符号位,如果有)。这样的成员称为位域;其宽度以冒号开头。

¶10 位域被解释为具有指定位数的带符号或无符号整数类型。125) 如果将0或1的值存储到类型为 _Bool 的非零宽度位域中,则位域的值应与存储的值相等;_Bool 位域具有 _Bool 的语义。

¶11 一种实现可以分配任何大小足以容纳位域的寄存器。如果有足够的空间,一个紧随结构体中另一个位域后面的位域应该被打包成同一寄存器的相邻位。如果剩余空间不足,无法放置的位域是被放置在下一个寄存器还是重叠在相邻寄存器上是由具体实现定义的。位域在寄存器内部的分配顺序(从高位到低位还是从低位到高位)也是由具体实现定义的。寄存器的对齐方式是未指定的。

¶12 没有声明符,只有冒号和宽度的位域声明表示未命名的位域。126)特殊情况下,宽度为0的位域结构成员表示在上一个位域所在的寄存器中不再放置更多的位域。

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

123)结构体或联合体不能包含可变类型的成员,因为成员名称不是如6.2.3中定义的普通标识符。

124)一元操作符&(地址运算符)不能应用于位域对象;因此,不存在指向或数组的位域对象。

125)如上6.7.2所述,如果实际使用的类型说明符是int或定义为int的typedef-name,则具体实现可以定义位域是否有符号。

126)未命名的位域结构成员对于填充以符合外部规定的布局很有用。

位域在单元内的分配顺序(高位到低位或低位到高位)是由实现定义的。

需要注意的是,“实现定义”意味着实现必须定义它的行为。也就是说,您可以查看文档,文档必须告诉您编译器执行的操作(如果编译器符合标准)。这与“未指定”和其他一些术语不同 - 编译器编写者几乎肯定不会轻易地从版本到版本改变位域的行为。相比之下,例如函数参数的求值方式可能会根据编译时选择的优化选项或每个版本的变化而异。

§6.5.2.2 函数调用

在函数设计器和实际参数的评估之后但在实际调用之前,存在一个序列点。在调用函数体执行之前或之后没有明确排序的调用函数中的每个评估与调用函数的执行被不确定排序。94)

94) 换句话说,函数执行不会相互插入。

6.7.2 类型说明符

每个逗号分隔的多重集合都指定相同的类型,但对于位域,实现定义说明符int是指定与signed int相同类型还是unsigned int相同类型。


你的意思是这会因编译器而异?我不应该依赖这个? - Jeegar Patel
@JonathanLeffler:我认为位域内存管理因机器而异,而不是编译器。同样的编译器在不同的机器上可能会表现出不同的行为,但反之则不会。 - Nitin Tripathi
你需要查看机器(硬件和操作系统组合)的ABI(应用程序二进制接口)是否规定了位域布局。如果是这样,编译器符合ABI的可能性很高。 如果ABI没有规定,不同的编译器可能会按照自己的意愿来进行; C标准表示它们可以这样做,而ABI并不能否决它。您仍然需要阅读编译器文档,以确定它是否遵循ABI(以及它按照哪个标准执行)。 - Jonathan Leffler
2
GCC文档指出,位域在单元内的分配顺序由ABI确定。ELF x86_64 psABI规定,位域从右到左进行分配。 - ninjalj
@ninjalj:谢谢,这是有用的信息。引用“标准的美妙之处在于有那么多可以选择”的俏皮话确实很诱人。很难知道编译器每个决策背后的标准是什么。 - Jonathan Leffler

2

位域是非可移植和机器相关的。

使用位域时,请注意以下问题:

  1. 代码将是非可移植的,因为字节内位和字节内部的组织形式是机器相关的
  2. 您不能取一个位域的地址;因此,如果x是一个位域标识符,则表达式&mystruct.x是非法的,因为无法保证mystruct.x位于一个字节地址上。
  3. 位域用于将更多的变量打包到较小的数据空间中,但会导致编译器生成额外的代码来操作这些变量。这在代码大小和执行时间方面都会造成代价。

1
只要是内部数据结构,并且您不额外努力检查位的顺序,代码就会是可移植的。 - Bo Persson
位域是标准的 C 工具,只要使用标准提供的内容,你的代码就可以移植。 - edmz

1

对于x86架构上的Linux系统,相关的ABI文档可以在这里(pdf)找到。

特别涉及到位域(bitfields):

"Plain" 位域(即既非“signed”也非“unsigned”的位域)始终具有非负值。尽管它们可能具有类型char、short、int或long(这些类型可以具有负值),但这些位域与相应的“unsigned”类型的位域具有相同的范围。
位域遵循与其他结构和联合成员相同的大小和对齐规则,以下是额外的规则:
- 位域从右到左分配(从最不重要的位到最重要的位)。 - 一个位域必须完全驻留在适合其声明类型的存储单元中。因此,位域永远不会跨越其单元边界。 - 位域可以与其他struct/union成员共享存储单元,包括不是位域的成员。当然,结构体成员占据存储单元的不同部分。未命名的位域类型不影响结构体或联合的对齐方式,尽管各个位域成员偏移量遵守对齐约束条件。"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接