位域
让人疑惑的C语言位域 - 知乎
Excerpt
在实际的应用中,有些数据的存储只需要几个二进制位,而不需要一个字节或几个字节,比如:电灯接通电源的状态,只有通电和未通电两种状态,用 1 和 0 就可以表示,为了满足这种需求,C 语言中引入了位域的概念 位…
在实际的应用中,有些数据的存储只需要几个二进制位,而不需要一个字节或几个字节,比如:电灯接通电源的状态,只有通电和未通电两种状态,用 1 和 0 就可以表示,为了满足这种需求,C 语言中引入了位域的概念
位域是什么
位域是一种数据结构,可以把数据以二进制位的形式紧凑的存储,它允许程序对此结构的位进行操作
在计算机早期,内存是非常稀缺的,需要尽可能的节省每一个字节,所以,C 语言中就出现了能针对二进制位进行操作的位域
为什么要用位域
位域这种数据结构,可以最大限度的节省存储空间,对于一些非常频繁的操作,需要尽可能的减少操作的数据,比如:在开发网络应用时,数据的序列化和反序列化是很频繁的,如果能减少数据的长度,对提升数据打包效率是很有帮助的
位域的出现,让我们可以用变量名代表某些bit,并通过变量名获取和设置 bit 的值,而不是通过晦涩难理解的位操作来进行,例如:
1 | struct field |
通过位域设置中间 2 个bit 的值,只需要设置结构体中 b1
字段值即可,如果使不用位域字段,就需要进行位的 “或” 和 “与” 运算
位域的使用
C 语言中,位域的表示形式如下
1 | struct bitfield |
b0、b1、b2 ... bn
表示位域成员,n0、n1、n2 ... nk
表示成员占用多少个 bit
位域表示的范围通常不能超过其所依附类型所能表示的 bit 数,比如:上面 bitfield
结构体中 位域所依附的类型是 unsigned int
, 最大能表示 32 个 bit,也就是说,n0、n1、n2 ... nk
总 bit 数不能超过 32,每个成员超过指定 bit 表示的最大数值时会被截断,具体请看下面的例子
1 | #include <stdio.h> |
用 gcc -g -o bitfield bitfield.c
编译并运行,结果为
1 | [root@localhost]# ./bitfield |
字段 a
赋值为 0x8F
对应的二进制为 1000 1111,由于 a
只有 7 个bit,给它赋的值超出了限定的位数,超出部分被丢弃,保留低 7 位,最终结果为 000 1111 ,换成十六进制是 0xf
字段 b
赋值为 0x1A
对应的二进制为 1 1010, b
包含 5 个bit,取结果中的低 5 位,最终结果为 1 1010 ,换成十六进制是 0x1a , 输出结果和赋值相同,即没有超出限定 bit 数
字段 c
赋值为 0x19
对应的二进制为 1 1001,由于 c
只有 4 个bit,给它赋的值超出了限定的位数,超出部分被丢弃,保留低 4 位,最终结果为 1001 ,换成十六进制是 0x9
位域的使用有一定的限制,机器最小粒度的寻址单位是字节,我们无法像获得某一个字节的地址一样去获得某个 bit 的地址,下面是一个错误的示例
1 | #include <stdio.h> |
上述代码功能是打印出成员 a
的地址,它无法通过编译,错误如下
1 | bitfield.c: 在函数‘main’中: |
位域的存储
C 标准中只允许 unsigned int
、signed int
、int
类型的位域申明,后面又增加了 bool
类型的支持,一些编译器像 gcc
、msvc
等自行加入了一些扩展,使得其他的类型(short、char
等)也支持位域
位域的存储跟编译器相关,不同的编译器,存储位域的方式可能不一样,总的来说可以分成下面几类
1、相邻位域成员,它们的类型相同时
如果它们的 bit 数之和小于等于所依附类型的 bit 数,那么,后面的成员紧接着前面的成员存储
如果它们的 bit 数之和大于所依附类型的 bit 数,那么超过的成员会存储到新的存储单元中,新存储单元会偏移成员所依附类型的 sizeof 字节数
以下面的代码为例来说明
1 | #include <stdio.h> |
编译运行,结果如下
1 | [root@localhost]# ./bitfield |
a 、b、c
字段都是 unsigned short
类型,它们的 bit 数之和为 10 + 4 + 2 = 16, 刚好等于 unsigned short 的 bit 数,所以它们会紧凑的存储,没有任何空隙
如果把 a
的 bit 数改成 11,即 **unsigned short a:11;**,此时,a
和 b
的 bit 数之和为 11 + 4 = 15,没有超过 unsigned short
的 bit 数
如果再加上 c
的 bit 数,结果变成了 17,超过了 unsigned short
的 bit 数,这种情况下,a
和 b
还是会紧凑的存储,而 c
会存储到新的存储单元中,新的存储单元字节数为 sizeof(unsigned short) = 2
, 所以此时 sizeof(fg) 是 4
2、相邻位域成员,它们的类型不同时
这种情况跟具体的编译器相关,以下面的代码为例来说明
1 | #include <stdio.h> |
上述代码分别用 gcc4.8.5 和 vs2013 进行编译运行,结果如下
gcc的结果
vs2013的结果
可以看到,当相邻位域成员所依附的类型不同时,不同的编译器产生的结果是不一样的
在 gcc 下的运行结果是 2 ,表示 a
和 b
还是紧凑存储的
而在 vs2013 下运行的结果是 4,这说明 a
和 b
完全按照他们所依附的类型来存储,此时位域没有进行压缩存储
3、位域成员之间存在非位域成员时
这种情况 gcc 和 vs2013 都不会进行压缩存储,按照内存对齐的规则来存储
还是以下面的代码为例来说明
1 | #include <stdio.h> |
上述代码分别用 gcc4.8.5 和 vs2013 进行编译运行,结果如下
gcc的结果
vs2013的结果
不管在 gcc 还是在 vs2013 下,结果都相同,为了提高访问效率,成员按照 4 字节对齐,所以 sizeof(fg)
结果是 12
现在位域使用得也比较少了,大概有以下几个的原因
1、早期计算机内存很稀缺,在内存的使用上需要精打细算,但是,现代的计算机内存容量有了很大的提升,一般不需要为了节省几个字节而使用内存更加紧凑的位域
2、通过前面的介绍,我们知道结构体中位域的存储是跟编译器相关,这就导致了它的可移植性比较差
匿名位域
位域成员可以不指定名字,只给出成员的数据类型以及占用的 bit 数,称作匿名位域
匿名位域字段只是起填充 bit,调整成员位置的作用,并无实际的意义
因为没有指定成员名字,所以也不能使用
1 | struct fields |
上面例子中,如果没有匿名位域的话,sizeof(fields)
的结果为 2,加入 6 个 bit 的填充以后,a
和 b
将分开存储, sizeof(fields)
的结果变成了 4
我们还可以通过匿名0长度的位域字段来强制位域存储到下一个存储单元中,例如:
1 | struct fields |
上面的结构体本来可以全部存储到一个 2 字节的存储单元中,如果我们想让 a
和 b
存储到不同的存储单元中,可以在结构体中加入一个匿名的 0 长度的位域字段来实现
1 | struct fields |
这样申明后,sizeof(struct fields)
就变成 4 了
小结
本文讲述了位域的基础,使用以及存储,其中位域的存储跟具体的编译器实现相关,这一点务必要注意,否则版本移植的时候要趟”坑”