C语言中的位字段

位字段(bit filed)是C语言中一种存储结构,不同于一般结构体的是它在定义成员的时候需要指定成员所占的位数。位字段是一个signed int或unsigned int类型变量中一组相邻的位(C99和C11新增了Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了4个1位的字段:

struct {

    unsigned autfd : 1;
    unsigned bldfc : 1;
    unsigned undln : 1;
    unsigned itals : 1;
} prnt;

根据该声明,prnt包含了4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:

    prnt.itals=1;
    prnt.undln=0;
    prnt.bldfc=1;
    prnt.autfd=0;

下面查看一下prnt的值:

    char str[33];
    int* value=reinterpret_cast<int*>(&prnt);
    itoa(*value,str,2);
    printf("%d %d %d %d\n", prnt.autfd,prnt.bldfc,prnt.undln,prnt.itals);
    printf("sizeof(prnt) = %d\n",sizeof(prnt));
    printf("十进制: %d\n",prnt);
    printf("二进制: %032s\n",str);

输出的结果为:
0 1 0 1
sizeof(prnt) = 4
十进制: 10
二进制: 0000 0000 0000 0000 0000 0000 0000 1010

可以看出,prnt的大小为4个字节(unsigned int 或 signed int),通过prnt的结构成员可以设置和访问某些bit位的值。
带有位字段的结构提供了一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二进制一。例如,开或关、真或假。如果只需要使用1位,就不需要使用整个变量。内含位字段的结构允许在一个存储单元中存储多个设置。
有时,某些设置也有多个选择,因此需要多位来表示。例如,可以使用如下代码:

struct{
    unsigned code1 : 2;
    unsigned code2 : 2;
    unsigned code3 : 8;
}prcode;

这里创建了两个2位的字段和一个8位的字段,可以这样赋值:

    prcode.code1=0;
    prcode.code2=3;
    prcode.code3=102;

但是要确保赋的值不超出字段可容纳的范围(下面会说明当超出范围时会发生什么事情)。

再次打印出prcode的内容

    int* value_prcode=reinterpret_cast<int*>(&prcode);
    itoa(*value_prcode,str,2);
    printf("%d %d %d\n",prcode.code1,prcode.code2,prcode.code3);
    printf("sizeof(prcode) = %d\n",sizeof(prcode));
    printf("十进制: %d\n",prcode);
    printf("二进制: %032s\n",str);

打印结果如下:
0 3 102
sizeof(prcode) = 4
十进制:1644
二进制:0000 0000 0000 0000 0000 0110 0110 1100

可以看出,prcode.code1对应于0-1比特位,数值为00,对应十进制为0;
prcode.code1对应于2-3比特位,数值为11,对应十进制为3;
prcode.code2对应于4-11比特位,数值为0110 0110,对应十进制为102。
因此,一个字段可以对应于多个比特位,且当使用结构字段赋值在可容纳的范围之内时,变量可以记录正确的值。

这里再讨论一些需要注意的问题。首先是,如果声明的总位数超过一个unsigned int类型的大小(4 bytes)时会发生什么事情?结果是会用到下一个unsigned int类型的存储位置。一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。例如:

struct{
    unsigned a : 4;
    unsigned b : 4;
    unsigned c : 4;
    unsigned d : 25;
}prlimit;

上面定义的位字段大小共37个bits,超过了一个unsigned int的范围,给prlimit的各结构成员赋值,并使用下面代码打印出prlimit所存储的内容:

    prlimit.a=0xF;
    prlimit.b=0;
    prlimit.c=0xF;
    prlimit.d=0x1FFFFFF;

    char str_1[33];
    char str_2[33];
    int* value_1=reinterpret_cast<int*>(&prlimit);
    int* value_2=reinterpret_cast<int*>(&prlimit)+1;
    itoa(*value_1,str_1,2);
    itoa(*value_2,str_2,2);
    printf("0x%x 0x%x 0x%x 0x%x \n",prlimit.a,prlimit.b,prlimit.c ,prlimit.d);
    printf("sizeof(prlimit) = %d\n",sizeof(prlimit));
    printf("二进制 0~31位: %032s\n",str_1);
    printf("二进制 32~63位: %032s\n",str_2);

输出的结果如下:
0xf 0x0 0xf 0x1ffffff
sizeof(prlimit) = 8
二进制 0-31位:0000 0000 0000 0000 0000 1111 0000 1111
二进制 32-63位:0000 0001 1111 1111 1111 1111 1111 1111

从输出的结果可以看出,首先,prlimit的大小为8个字节;其次,编译器强制prlimit.d字段发生边界对齐,即prlimit.d位于第二个unsigned int上,prlimit.c与prlimit.d之间会填充未命名的“洞”。

实际上,我们也可以人为的在结构体当中设置未命名的字段宽度来进行填充。使用一个宽度为0的未命名的字段迫使下一个字段与下一个整数对齐:

struct {
    unsigned field1 : 1;
    unsigned        : 2;
    unsigned field2 : 1;
    unsigned        : 0;
    unsigned field3 : 4;
} stuff;

使用下面的代码输出stuff的内容:

    stuff.field1=1;
    stuff.field2=1;
    stuff.field3=0xf;
    char str_1[33];
    char str_2[33];
    int *value_1=reinterpret_cast<int*>(&stuff);
    int *value_2=reinterpret_cast<int*>(&stuff)+1;
    itoa(*value_1,str_1,2);
    itoa(*value_2,str_2,2);
    printf("sizeof(stuff) = %d\n",sizeof(stuff));
    printf("二进制 0-31位: %032s\n",str_1);
    printf("二进制 32-63位: %032s\n",str_2);

输出的结果为:
sizeof(stuff) = 8
二进制 0-31位:0000 0000 0000 0000 0000 0000 0000 1001
二进制 32-63位: 0000 0000 0000 0000 0000 0000 0000 1111

也就是说,在这里,stuff.field1和stuff.field2之间,有一个2位的空隙;stuff.field3被强迫与下一个整数对齐,存储到下一个unsigned int中。stuff的大小为8个字节。

最后讨论一个当赋值超出字段可容纳范围的问题。

struct {
    unsigned t1 : 2;
    unsigned t2 : 3;
    unsigned t3 : 4;
} test;

int main(){

    test.t1=3;
    test.t3=0;
    test.t2=11;
    printf("%d %d %d\n",test.t1,test.t2,test.t3);
    int *value=reinterpret_cast<int*>(&test);
    char str[50];
    itoa(*value,str,2);
    printf("二进制:%032s\n",str);

    return 0;

}

输出的结果为:
3 3 0
二进制:0000 0000 0000 0000 0000 0000 0000 1111

上述代码中,成员test.t2赋值的大小超出了容纳的范围。可以看到,t2赋值为11(二进制是1011),结果输出的值是3(二进制是011),即截断了超出的部分。同时也可以看到,超出的部分不会影响t3的值(不同平台不一样?网上有人说会覆盖超出的区域)。

最后需要说明的是,字段存储在一个int中的顺序取决于机器。在有些机器上,存储的顺序是从左往右的,而在另一些机器上,是从右往左的。另外,不同的机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植。尽管如此,有些情况却要用到这种不可移植的特性。例如,以特定硬件设备所用的形式存储数据。

参考资料:
《C Primer Plus》(第6版)

你可能感兴趣的:(C语言中的位字段)