位域(bit-field) 是一种特殊的结构体成员,在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit)。
什么是位域?
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为 “位域” 或 “位段”。
位域声明
位域的定义与结构体相仿:
struct 结构名 {
//位域列表
type member_name : width; //width位宽:位域中位的数量
...
};
:
后的数字用来限定成员变量占用的位数,带有与定义宽度的变量称为位域。看下面的例子:
struct bs {
unsigned m;
unsigned int n : 5;
unsigned char ch : 6;
};
成员m
没有限制,根据数据类型即可推算出它占用4个字节(Byte)的内存。成员 n
、ch
被位宽限制,不能再根据数据类型计算长度,它们分别占用 5、6 位(Bit)的内存。
n
、ch
的取值范围非常有限,数据稍微大些就会发生溢出:
int main(void) {
//赋值
struct bs {
unsigned m;
unsigned int n : 5;
unsigned char ch : 6;
} data = {0xad, 0xe, '$'};
//第一次输出
printf("%#x, %#x, %c = %d\n", data.m, data.n, data.ch, data.ch);
//以十六进制数出一个整型
//更改值后第一次输出
data.m = 0xb8901c;
data.n = 0x2d;
data.ch = 'z';
printf("%#x, %#x, %c\n", data.m, data.n, data.ch);
//printf("%d %c", 0x2d - 0xd, 58);
}
运行结果:
对于n
和ch
,第二次输出的数据是残缺的。这里编译器也给出了提示:
意为检测到将一个值为122的整数(z
)赋给一个位域类型变量,但是位域的范围不足以容纳122,因此将值截断为58。
- 第一次输出时,
n
、ch
的值分别是0xe、0x24($
对应的 ASCII 码为0x24),换算成二进制是1110、100100,都没有超出限定的位数,能够正常输出。 - 第二次输出时,
n
、ch
的值变为0x2d、0x7a(z
对应的 ASCII 码为0x7a),换算成二进制分别是101101、1111010,都超出了限定的位数。超出部分被直接截去,剩下11010、111010,换算成十六进制为0xd、0x3a(0x3a对应的字符是:
)。
C语言标准规定,位域的宽度不能超过它所依附数据类型的长度,这个类型限制了成员变量的最大长度,
:
后面的数字不能超过这个长度。
比如在上面的示例中,n
是unsigned int
类型,长度为4个字节,那么给n指定占位数时,位宽就不能超过32,ch
是char
类型,长度为1个字节,其位宽就不能超过8。
所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。我们可以这样认为,位域技术就是在成员变量所占用的内存中选出一部分位宽来存储数据。
C语言标准还规定,只有有限的几种数据类型可以用于位域。在ANSI C中,这几种数据类型是
int
、signed int
(int)和unsigned int
;到了C99,_Bool
也被支持了。
但编译器在具体实现时都进行了扩展,额外支持了char、signed char、unsigned char以及enum类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
位域的存储
-
当相邻成员的类型相同时:
如果它们的位宽之和小于该类型的sizeof
大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的sizeof
大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。int main(void) { struct bss { unsigned m : 6; unsigned n : 12; unsigned p : 4; }; printf("%lu\n", sizeof(struct bss)); // 输出:4 }
m、n、p的类型都是
unsigned int
,sizeof的结果为4个字节(Byte),即32个位(Bit)。m、n、p的位宽之和为6 + 12 + 4 = 22
,小于32,所以它们会挨着存储,中间没有缝隙。之所以输出4,而不是3,是因为要将内存对其到4个字节,以便提高存取效率。如果将成员m的位宽改为22,那么输出结果将会是8,因为
22 + 12 = 34
,大于32,n会从新的位置开始存储,相对m的偏移量是sizeof(unsigned int)
,也即4个字节。
如果再将成员p的位宽也改为22,那么输出结果将会是12,三个成员都不会挨着。 -
当相邻成员类型不同时:
不通过的编译器有不同的实现方案,GCC
会压缩存储,而VC/VS
不会。int main(){ struct bs{ unsigned m: 12; unsigned char ch: 4; unsigned p: 4; }; printf("%d\n", sizeof(struct bs)); return 0; }
在GCC下的运行结果为 4,三个成员挨着存储;在VC/VS下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。
如果成员之间穿插着非位域成员,那么不会进行压缩,如下:
struct bs{ unsigned m: 12; unsigned ch; unsigned p: 4; };
在各个编译器下
sizeof
的结果都是12。通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&
获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
无名位域(空域)
位域成员可以没有名称,只给出数据类型和位宽:
struct bs{
int m: 12;
int : 20; //该位域成员不能使用,这20位填0
int n: 4; //从下一单元开始存放
};
无名位域一般用来作填充或调整成员位置。
上面的例子中,如果没有位宽为20的无名成员,m、n将会挨着存储,sizeof(struct bs)的结果为 4;有了这20位作为填充,m、n将分开存储,sizeof(struct bs) 的结果为 8。
位域的使用
位域的使用和结构成员的使用相同,一般用.
或->
形式访问,位域允许用各种格式输出。
#include <stdio.h>
int main(){
struct bs{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit, *pbit;
bit.a = 1; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
bit.b = 7; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
bit.c = 15; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
printf("%d, %d, %d\n", bit.a, bit.b, bit.c); /* 以整型量格式输出三个域的内容 */
pbit = &bit; /* 把位域变量 bit 的地址送给指针变量 pbit */
pbit->a = 0; /* 用指针方式给位域 a 重新赋值,赋为 0 */
pbit->b& = 3; /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */
pbit->c |= 1; /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */
printf("%d, %d, %d\n", pbit->a, pbit->b, pbit->c); /* 用指针方式输出了这三个域的值 */
}
上面的程序中bs类型的变量bit
和指向bs
类型的指针变量pbit
,这说明位域也是可以使用指针的。