什么是位域
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个bit。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 bit即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为"位域"或"位段"。
所谓"位域"是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
典型的应用场景:
- 用 1 位二进位存放一个开关量时,只有 0 和 1 两种状态。
- 读取外部文件格式——可以读取非标准的文件格式。例如:9 位的整数。
- 结构体中部分成员可能的赋值较小只需要用到1位或几位,可以把这类成员凑到一起减少整个结构体占用的空间。
- 可巧妙用于位操作
位域的定义
位域定义与结构定义相仿,其形式为:
struct 位域结构名
{
位域列表
};
其中位域列表的形式为:
type [member_name] : width ;
其中 type :类型说明符; member_name: 位域名; width :位域长度;
例如:
struct TESTA
{
char a:3;
char b:5;
char c:4;
char d:4;
};
char 类型变量大小为一个字节(八位), 取值范围为(-128, 127)。在上述位域的定义中,
a 只取三位,取值范围为(-4, 3)。如果 a 为无符号型( unsigned char), 则取值范围为(0, 7)。
位域的使用
位域的使用和结构成员的使用相同,其一般形式为:
位域变量名.位域名
或
位域变量指针名->位域名
位域允许用各种格式输出。
示例:
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); /* 用指针方式输出了这三个域的值 */
}
结果:
使用位域的注意点(重要)
1、位域成员必须声明为整型int、unsigned int或signed int类型,或是char,unsigned char,但不能是浮点型包括float,double
在ANSI C 中,这几种数据类型是signed int和unsigned int;到了C99、C11新增了_Bool 的位字段。支持char一般是由于编译器扩充的。
2、位域的长度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度。
例如:
struct k{
int a : 33;
}
这里就会报错。
3、位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的(没有名称当然没法访问)。例如:
struct k{
int a:1;
int :2; /* 该 2 位不能使用 */
int b:3;
int c:2;
};
4、当无名位域且长度度为0时,表示下一个变量从下一段地址开始存储。(下一段地址的具体位置还得根据结构体的内存对齐原则)
示例:
#include <stdio.h>
struct k {
char a : 2;
char : 0;
char b : 2;
char c : 2;
};
struct k test;
int main() {
char* ptr = (char *)&test;
test.a = 1;
test.b = 1;
test.c = 1;
printf("siezof(test)=%d\n",sizeof(test));
printf("*ptr=%x ptr=%p\n", *ptr,ptr);
printf("*ptr=%x ptr=%p\n", *(ptr+1), ptr + 1);
}
结果:
上述例程中,位域a独占一个字节,b和c共占一个字节。
b直接从下一个字节地址开始存储了。
5、不能使用位域的地址,如:
struct k {
char a : 2;
char : 0;
char b : 2;
char c : 2;
};
struct k test;
printf("&test.a=%p\n",&test.a);//报错 不能使用位域的地址
6、当结构体位域成员超出了限定的位数,将发生上溢(溢出中的一种)。
#include <stdio.h>
struct
{
unsigned int age : 3;
} Age;
int main()
{
unsigned int* ptr = &Age;
Age.age = 4;
printf("Sizeof( Age ) : %d\n", sizeof(Age));
printf("Age.age : %d\n", Age.age);
Age.age = 7;
printf("Age.age : %d\n", Age.age);
Age.age = 8; // 二进制表示为 1000 有四位,超出了定义的3位长度
printf("Age.age : %d\n", Age.age);
printf("*p=%x\n", *ptr);
return 0;
}
结果:
注意,从最后的*p=0可以看出,溢出没有导致其他位发现改变,否则这里就会等于8了。
7、位域可以和正常的结构体成员写到一起。如:
struct
{
unsigned int age1 : 3;
unsigned int age2 : 3;
unsigned int age3;
unsigned int age4 : 3;
} Age;
8、一个位域存储在N个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。即不能跨字节存储位域。
举例:
struct pack
{
unsigned int a:12;
unsigned int b:24;
unsigned int c:6;
};
sizeof(struct pack) = 8
一个unsigned int是4字节,a占了12位,还剩20字节,b需要24位,已经不够了,又不能跨字节,因此b只能存在下一个unsigned int。
程序验证:
struct pack
{
unsigned int a : 12;
unsigned int b : 24;
unsigned int c : 6;
};
struct pack test;
int main()
{
unsigned int* ptr = (unsigned int *)&test;
test.a = 0xFFF;
test.b = 0xFFFFFF;
test.c = 0x3F;
printf("*ptr=%08X\n", *ptr);
printf("*(ptr+1)=%08X\n", *(ptr+1));
}
结果:
由上面的测试可以验证,a和b之间的确有空的区域,即结果中的00000部分;
9、整个结构体空间占用大小
含位域的结构体占用存储大小规则大致如下:
- 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止
- 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
- 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++,GCC采取压缩方式;
- 如果位域字段之间穿插着非位域字段,则不进行压缩(非位域字段保持其类型的对应大小);
- 整个结构体的总大小为最宽基本类型成员大小的整数倍。
第1,2点规则实际就是上面第8点所说的注意项问题,参考上文的示例即可。
第3点相邻的位域字段的类型不同,则各编译器的具体实现有差异举例:
#include <stdio.h>
struct test {
char a : 2;
char b : 3;
int c : 1;
};
int main(void)
{
printf("%d\n", sizeof(struct test));
return 0;
}
在Visual Studio 2019上运行结果:
在Linux上GCC编译运行结果:
4
(gcc version:4.8.2)
第四点如果位域字段之间穿插着非位域字段,则不进行压缩,指的是非位域字段不压缩
#include <stdio.h>
struct test {
char a : 2;
char b : 3;
int d ;
int c : 1;
};
int main(void)
{
printf("%d\n", sizeof(struct test));
return 0;
}
如上述示例程序struct test的d就是插在位域中的非位域字段,它的类型是int,所以d占4个字节,a和b共占4个字节,c单独占4个字节,因此sizeof(struct test) = 12
对于有位域的结构体占用存储大小的问题,做一个精简的方法总结:
带有’位域’的结构体并不是按照每个域对齐的,而是将一些位域 成员’捆绑’在一起做对齐的。"捆绑"还需注意跨字节问题,不能跨字节存储位域。位域捆绑后,按照普通结构体占用大小的规则计算即可。
实际应用
示例:
#include <stdio.h>
union STATE
{
struct BITDATA
{
int D0 : 1;
int D1 : 1;
int D2 : 1;
int D3 : 1;
int D4 : 1;
int D5 : 1;
int D6 : 1;
int D7 : 1;
}BIT;
int value;
};
int main(void)
{
int a = 0xFF;
union STATE* sta;
sta = &a;
printf("sta.value=%02X\n",sta->value);
printf("sizeof=%d\n", sizeof(sta->value));
sta->BIT.D0 = 0;//给第一个位赋值
printf("sta.value=%X\n", sta->value);
printf("a=%X\n", a);
return 0;
}
上述例程,使用union和位域实现对变量的某一bit位进行操作;该方式可用于计算机操作底层硬件寄存器。
结果:
参考:
https://blog.51cto.com/u_15244533/2845234
https://blog.51cto.com/u_14207158/2352294
https://blog.51cto.com/u_9233403/2121352
https://www.runoob.com/cprogramming/c-bit-fields.html
https://blog.csdn.net/sty124578/article/details/79456405