位域

位域

让人疑惑的C语言位域 - 知乎

Excerpt

在实际的应用中,有些数据的存储只需要几个二进制位,而不需要一个字节或几个字节,比如:电灯接通电源的状态,只有通电和未通电两种状态,用 1 和 0 就可以表示,为了满足这种需求,C 语言中引入了位域的概念 位…


在实际的应用中,有些数据的存储只需要几个二进制位,而不需要一个字节或几个字节,比如:电灯接通电源的状态,只有通电和未通电两种状态,用 1 和 0 就可以表示,为了满足这种需求,C 语言中引入了位域的概念

位域是什么

位域是一种数据结构,可以把数据以二进制位的形式紧凑的存储,它允许程序对此结构的位进行操作

在计算机早期,内存是非常稀缺的,需要尽可能的节省每一个字节,所以,C 语言中就出现了能针对二进制位进行操作的位域

为什么要用位域

位域这种数据结构,可以最大限度的节省存储空间,对于一些非常频繁的操作,需要尽可能的减少操作的数据,比如:在开发网络应用时,数据的序列化和反序列化是很频繁的,如果能减少数据的长度,对提升数据打包效率是很有帮助的

位域的出现,让我们可以用变量名代表某些bit,并通过变量名获取和设置 bit 的值,而不是通过晦涩难理解的位操作来进行,例如:

1
2
3
4
5
6
7
8
9
struct field
{
unsigned char b0 : 3,
b1 : 2,
b2 : 3;
};

struct field bf;
bf.b1 = 3;

通过位域设置中间 2 个bit 的值,只需要设置结构体中 b1 字段值即可,如果使不用位域字段,就需要进行位的 “或” 和 “与” 运算

位域的使用

C 语言中,位域的表示形式如下

1
2
3
4
5
6
7
8
struct bitfield
{
unsigned int b0 : n0,
b1 : n1,
b2 : n2,
...
bn : nk;
};

b0、b1、b2 ... bn 表示位域成员,n0、n1、n2 ... nk 表示成员占用多少个 bit

位域表示的范围通常不能超过其所依附类型所能表示的 bit 数,比如:上面 bitfield 结构体中 位域所依附的类型是 unsigned int, 最大能表示 32 个 bit,也就是说,n0、n1、n2 ... nk 总 bit 数不能超过 32,每个成员超过指定 bit 表示的最大数值时会被截断,具体请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
struct fields
{
unsigned short a:7;
unsigned short b:5;
unsigned char c:4;

}fs;

fs.a = 0x8F;
fs.b = 0x1A;
fs.c = 0x19;

printf("%#x, %#x, %#x \n",fs.a, fs.b, fs.c);

}

gcc -g -o bitfield bitfield.c 编译并运行,结果为

1
2
[root@localhost]# ./bitfield                   
0xf, 0x1a, 0x9

字段 a 赋值为 0x8F 对应的二进制为 1000 1111,由于 a 只有 7 个bit,给它赋的值超出了限定的位数,超出部分被丢弃,保留低 7 位,最终结果为 000 1111 ,换成十六进制是 0xf

字段 b 赋值为 0x1A 对应的二进制为 1 1010b 包含 5 个bit,取结果中的低 5 位,最终结果为 1 1010 ,换成十六进制是 0x1a , 输出结果和赋值相同,即没有超出限定 bit 数

字段 c 赋值为 0x19 对应的二进制为 1 1001,由于 c 只有 4 个bit,给它赋的值超出了限定的位数,超出部分被丢弃,保留低 4 位,最终结果为 1001 ,换成十六进制是 0x9

位域的使用有一定的限制,机器最小粒度的寻址单位是字节,我们无法像获得某一个字节的地址一样去获得某个 bit 的地址,下面是一个错误的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
struct fields
{
unsigned short a:7;
unsigned short b:5;
unsigned char c:4;
}fs;

printf("%p\n",&fs.a);

}

上述代码功能是打印出成员 a 的地址,它无法通过编译,错误如下

1
2
3
4
bitfield.c: 在函数‘main’中:
bitfield.c:11:5: 错误:无法取得位段‘a’的地址
printf("%p\n",&fs.a);
^

位域的存储

C 标准中只允许 unsigned intsigned intint 类型的位域申明,后面又增加了 bool 类型的支持,一些编译器像 gccmsvc等自行加入了一些扩展,使得其他的类型(short、char等)也支持位域

位域的存储跟编译器相关,不同的编译器,存储位域的方式可能不一样,总的来说可以分成下面几类

1、相邻位域成员,它们的类型相同时

如果它们的 bit 数之和小于等于所依附类型的 bit 数,那么,后面的成员紧接着前面的成员存储

如果它们的 bit 数之和大于所依附类型的 bit 数,那么超过的成员会存储到新的存储单元中,新存储单元会偏移成员所依附类型的 sizeof 字节数

以下面的代码为例来说明

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
struct flag
{
unsigned short a:10;
unsigned short b:4;
unsigned short c:2;
}fg;
printf("%d\n", sizeof(fg));
return 0;
}

编译运行,结果如下

1
2
[root@localhost]# ./bitfield                   
2

a 、b、c 字段都是 unsigned short 类型,它们的 bit 数之和为 10 + 4 + 2 = 16, 刚好等于 unsigned short 的 bit 数,所以它们会紧凑的存储,没有任何空隙

如果把 a 的 bit 数改成 11,即 **unsigned short a:11;**,此时,ab 的 bit 数之和为 11 + 4 = 15,没有超过 unsigned short 的 bit 数

如果再加上 c 的 bit 数,结果变成了 17,超过了 unsigned short 的 bit 数,这种情况下,ab 还是会紧凑的存储,而 c 会存储到新的存储单元中,新的存储单元字节数为 sizeof(unsigned short) = 2, 所以此时 sizeof(fg)4

2、相邻位域成员,它们的类型不同时

这种情况跟具体的编译器相关,以下面的代码为例来说明

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main()
{
struct flag
{
unsigned short a:10;
unsigned char b:4;
}fg;
printf("%d\n", sizeof(fg));
return 0;
}

上述代码分别用 gcc4.8.5 和 vs2013 进行编译运行,结果如下

gcc的结果

vs2013的结果

可以看到,当相邻位域成员所依附的类型不同时,不同的编译器产生的结果是不一样的

在 gcc 下的运行结果是 2 ,表示 ab 还是紧凑存储的

而在 vs2013 下运行的结果是 4,这说明 ab 完全按照他们所依附的类型来存储,此时位域没有进行压缩存储

3、位域成员之间存在非位域成员时

这种情况 gcc 和 vs2013 都不会进行压缩存储,按照内存对齐的规则来存储

还是以下面的代码为例来说明

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
struct flag
{
unsigned short a:10;
unsigned int i;
unsigned char b:4;
}fg;
printf("%d\n", sizeof(fg));
return 0;
}

上述代码分别用 gcc4.8.5 和 vs2013 进行编译运行,结果如下

gcc的结果

vs2013的结果

不管在 gcc 还是在 vs2013 下,结果都相同,为了提高访问效率,成员按照 4 字节对齐,所以 sizeof(fg) 结果是 12

现在位域使用得也比较少了,大概有以下几个的原因

1、早期计算机内存很稀缺,在内存的使用上需要精打细算,但是,现代的计算机内存容量有了很大的提升,一般不需要为了节省几个字节而使用内存更加紧凑的位域

2、通过前面的介绍,我们知道结构体中位域的存储是跟编译器相关,这就导致了它的可移植性比较差

匿名位域

位域成员可以不指定名字,只给出成员的数据类型以及占用的 bit 数,称作匿名位域

匿名位域字段只是起填充 bit,调整成员位置的作用,并无实际的意义

因为没有指定成员名字,所以也不能使用

1
2
3
4
5
6
7
struct fields
{

unsigned short a:10,
:6; //匿名位域,不能使用
unsigned short b:3;
};

上面例子中,如果没有匿名位域的话,sizeof(fields) 的结果为 2,加入 6 个 bit 的填充以后,ab 将分开存储, sizeof(fields) 的结果变成了 4

我们还可以通过匿名0长度的位域字段来强制位域存储到下一个存储单元中,例如:

1
2
3
4
5
struct fields
{
unsigned short a:10;
unsigned short b:3;
};

上面的结构体本来可以全部存储到一个 2 字节的存储单元中,如果我们想让 ab 存储到不同的存储单元中,可以在结构体中加入一个匿名的 0 长度的位域字段来实现

1
2
3
4
5
6
struct fields
{
unsigned short a:10;
unsigned short :0;
unsigned short b:3;
};

这样申明后,sizeof(struct fields) 就变成 4

小结

本文讲述了位域的基础,使用以及存储,其中位域的存储跟具体的编译器实现相关,这一点务必要注意,否则版本移植的时候要趟”坑”