C语言位域

位域概述

位域是为了将所需的存储量减少到最小而设计的,有些数据在存储时并不需要占用一个完整的字节,只需占用一个或多个 bit 位。基于这种考虑,将相同的存储位置划分为“位字段”,而不是每个位字段指定专用的位置。

摘自维基百科:

位段(或称“位域”,Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的好处:

  • 可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
  • 位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。

位域的运用

先来看个 Demo:

1
2
3
4
5
6
typedef struct demo
{
unsigned char a:4;
unsigned char b:3;
unsigned char c:1;
} demo_t;

在结构体中声明位域要使用 : 运算符,后边的数字用于限定结构体成员变量所占用的位数。

上边的例子将创建三个宽度为 431 位的无符号字符。位字段的作用基本上是屏蔽按位操作以访问其字段的值。它们实际上是具有特定长度(demo 占用 1 字节)的内存地址。

若结构体 Demo 不声明位域的话,成员 a、b 和 c 应占用 3 个字节,但是实际上我们将其设置为宽度为 431 的位,所以总得到的其占用长度为 1 个字节(4bit + 3bit + 1bit = 8bit = 1byte)。

我们写个程序验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

typedef struct demo
{
unsigned char a:4;
unsigned char b:3;
unsigned char c:1;
} demo_t;

typedef struct demo1
{
unsigned char a;
unsigned char b;
unsigned char c;
} demo1_t;


int main(void)
{
printf("demo = %d\n", sizeof(demo_t));
printf("demo1 = %d\n", sizeof(demo1_t));
return 0;
}

输出结果为:

1
2
demo = 1
demo1 = 3

unsigned char 占用 1 字节,所以将成员变量 a 设置位数超过 8 位时会报错,如:

1
2
3
4
5
6
typedef struct demo
{
unsigned char a:9;
unsigned char b:3;
unsigned char c:1;
} demo_t;

编译时会报错:

1
error C2034: 'a' : type of bit field too small for number of bits

并且,我们改变了成员变量的占用字数,那么会影响到该变量的取值范围,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

typedef struct demo
{
unsigned char a:4;
unsigned char b:3;
unsigned char c:1;
} demo_t;

typedef struct demo1
{
unsigned char a;
unsigned char b;
unsigned char c;
} demo1_t;

int main(void)
{
demo_t demo1;
demo1_t demo2;

demo1.a = 255;
demo1.b = 255;
demo1.c = 255;
demo2.a = 255;

printf("demo1.a = %d\n", demo1.a);
printf("demo1.b = %d\n", demo1.b);
printf("demo1.c = %d\n", demo1.c);
printf("demo2.a = %d\n", demo2.a);
return 0;
}

输出结果为:

1
2
3
4
demo1.a = 15
demo1.b = 7
demo1.c = 1
demo2.a = 255

因为 demo2.a 占了 8 位二进制,最大能存储 255,而 demo1.a 占了 4 位二进制,最大只能存储 15,多的位都被舍弃了,同理 demo1.bdemo1.c 也因为所占位数小了,而只能存储该二进制的最大位值。

\bit 7 6 5 4 3 2 1 0 value
255 1 1 1 1 1 1 1 1 255
demo2.a 1 1 1 1 1 1 1 1 255
demo1.a \ \ \ \ 1 1 1 1 15
demo1.b \ \ \ \ \ 1 1 1 7
demo1.c \ \ \ \ \ \ \ 1 1

所以使用位域时,需注意该情况,避免不必要的排错麻烦。

当相邻成员使用和不使用位域的情况,我们先看使用位域的两种不同结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>

struct demo_t
{
int a:12;
int b:32;
int c:4;
}demo;

struct demo1_t
{
int a:12;
int b;
int c:4;
}demo1;

int main(void)
{
printf("demo = %d\n", sizeof(demo));
printf("demo1 = %d\n", sizeof(demo1));
return 0;
}

输出结果:

1
2
demo = 12
demo1 = 12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct demo_t
{
int a:12;
int b:32;
int c:4;
}__attribute__((packed)) demo;

struct demo1_t
{
int a:12;
int b;
int c:4;
}__attribute__((packed)) demo1;

int main(void)
{
printf("demo = %d\n", sizeof(demo));
printf("demo1 = %d\n", sizeof(demo1));
return 0;
}

输出结果:

1
2
demo = 6
demo1 = 7

上面两个例子中,当使用 __attribute__((packed)) 告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐时,两种相同的结构,输出的大小却不一样。

因为结构体 demo 中具有三个连续的位域,而 demo1 中是位域-非位域-位域,连续的位域会合并存储,而位域后紧跟着非位域的位字段时,必须与字节边界对齐,所以编译器无法将非位域的 int b 打包在位域中。若不设置 __attribute__((packed)),则编译器会优化对齐,所以获取实际大小时,输出的大小是相同的。

下图展示了实际存储的结构:

无名位域

如果位域的定义没有给出标识符名字,那么这是无名位域,无法被初始化。无名位域用于填充(padding)内存布局。只有无名位域的比特数可以为 0。这种占 0 比特的无名位域,用于强迫下一个位域在内存分配边界对齐。

同样拿刚才的 demo 演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct demo_t
{
int a:12;
int :4;
int b:32;
int c:4;
}__attribute__((packed)) demo;

struct demo1_t
{
int a:12;
int b;
int c:4;
}__attribute__((packed)) demo1;


int main(void)
{
printf("demo = %d\n", sizeof(demo));
printf("demo1 = %d\n", sizeof(demo1));
return 0;
}

输出结果:

1
2
demo = 7
demo1 = 7

无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域是不能使用的。我们在结构体 demo 中使用无名来使得成员 a 刚好与字节边界对齐,所以成员 b 可以紧跟着成员 a,这样使得 demodemo1 存储的结构一致,所以他们的大小也就相等了。

下图展示了实际存储的结构:

位域的优缺点

优点

  • 将由几个 bit 组成的对象打包成一个整数。这使得读/写和其他操作就像它们是 int 一样工作。
  • 在编写加密/解密的例程时,使用位域会有奇效。
  • 节省存储空间,而且处理简便。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

缺点

  • 不可移植,因为字节(byte)中的位(bit)和字(word)中的字节(byte)的放置顺序是取决于编译器的。
  • 机器的大小端,得出的结果可能在高低位地址实际排放的结果会相反。
  • 不能使用 & 获取位域成员的地址,因为不能保证位域成员在字节(byte)地址上。
  • 位字段用于将更多的变量打包到更小的数据空间中,会导致编译器产生额外的代码来操作这些变量。这在代码大小和执行时间方面都会付出代价。(编译器必须进行位分割/错误对齐的访问)

作为程序员,没有太大的区别。但是,与访问单个位相比,访问整个字节的代码要简单/短得多,因此使用位域会增加所生成的代码量。

访问整个字节:

1
2
3
ldb input1,b         ; get the new value into accumulator b
movb b,value1 ; put it into the variable
rts ; return from subroutine

使用位域:

1
2
3
4
5
6
7
8
9
10
11
    ldb input1,b        ; get the new value into accumulator b
movb bitfields,a ; get current bitfield values into accumulator a
cmpb b,#0 ; See what to do.
brz clearvalue1: ; If it's zero, go to clearing the bit
orb #$80,a ; set the bit representing value1.
bra resume: ; skip the clearing code.
clearvalue1:
andb #$7f,a ; clear the bit representing value1
resume:
movb a,bitfields ; put the value back
rts ; return