前言
说到位域,不得不说这是一个很有意思的东西,我平时用得不太多,因为这个东西在我的印象里是对内存抠到极致迫不得已才会使用它的。一般情况下我使用的硬件环境都还比较资源够用,所以就不怎么用,但是既然提到了,也就写一篇关于位域的文,大概从以下几个方面来说:
一、基本形式
位域定义与结构定义相仿,其形式为:
struct 位域结构名
{
位域列表
};
其中位域列表的形式为:
类型说明符 位域名: 位域长度
例如:
typedef struct bs
{
unsigned char a:1;
unsigned char b:3;
unsigned char c:4;
}Bits;
void test2(void)
{
Bits bit = {0};
bit.a = 1;
bit.b = 4;
bit.c = 15;
printf("a:%d, b:%d, c:%d.\n", bit.a, bit.b, bit.c);
printf("sizeof(bit):%d.\n", sizeof(bit));
}
//程序实际输出: a:1, b:4, c:15. sizeof(bit):1.
//在这个位域定义中,其中位域a占1位,位域b占3位,位域c占4位,共占1个字节空间(即8bit)
二、存储方式
1、位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。
例如:
typedef struct bs
{
unsigned char a:4;
unsigned char :2; /*空域,该2位不可以使用*/
unsigned char b:2;
}Bits;
2、一个位域必须存储在同一个基本类型所占的空间中,不能跨两个基本类型所占的空间。
如果一个基本类型所占的空间所剩空间不够存放另一位域时,应从下一单元起,开始存放该位域。也可以有意使某位域从下一单元开始。例如:
typedef struct bs
{
unsigned char a:4;
unsigned char :0; /*空域*/
unsigned char b:4; /*从下一单元开始存放*/
}Bits;
void test(void)
{
Bits bit = {0};
bit.a = 15;
bit.b = 15;
printf("a:%d, b:%d.\n", bit.a, bit.b);
printf("sizeof(bit):%d.\n", sizeof(bit));
}
// 程序实际输出:a:15, b:15. sizeof(bit):2.
//在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,
//b从第二字节开始,占用4位。所以该位域变量占2个字节空间。
3、位域的长度不能大于一个 位域基本类型所占的空间大小 的bit位数。
由于位域不允许跨两个位域基本类型所占的空间,因此位域的长度不能大于一个位域基本类型所占的空间大小的bit位数,也就是说char 类型的位域不能超过8位,short 类型的位域不能超过16位,int 类型的位域不能超过32位。
4、位域字段在内存中的位置是按照从低位向高位的顺序放置的
例如:
typedef struct bs { unsigned char a:2; //最低位; unsigned char b:3; unsigned char c:3; //最高位; }Bits; typedef union uni { Bits bit; unsigned char n; }Union; void test(void) { Union Uni; Uni.n = 0; //初始化; Uni.bit.a = 0; //二进制为: 00 Uni.bit.b = 0; //二进制为: 000 Uni.bit.c = 1; //二进制为: 001 printf("Uni.n = %d.\n", Uni.n); } // 程序实际输出: Uni.n = 32. /* 位域中的位域字段按照从低位向高位顺序方式的顺序来看。那么, a、b、c 这三个位域字段在内存中的放置情况是:最高位是c:001, 中间位是b:000,最低位是a:00。所以,这个位域结构中的8二进制内容 就是:0010 0000,总共8位,其十进制格式就是32。 又因为Uni为联合体,并且我的电脑输出为“小端模式”,低地址存放低位数据, 故Uni.n = 0010 0000,即32 */
再例如:想获得0x5B 59 97 85,采用小端程序应该是:
typedef struct little_endian { int G:11; // 低字节数 int F:6; // | int E:5; // | int D:4; // | int C:3; // | int B:2; // V int A:1; // 高字节数 }little_endian; int main() { little_endian LittleEndian; LittleEndian.G = 0x0785;//0b11110000101 低字节数 LittleEndian.F = 0x32;//0b110010 | LittleEndian.E = 0x0C;//0b01100 | LittleEndian.D = 0x0D;//0b1101 | LittleEndian.C = 0x06;//0b110 | LittleEndian.B = 0x02;//0b10 V LittleEndian.A = 0x00;//0b0 高字节数 printf("0x%x",LittleEndian); return 0; } //输出 0x5B599785 (即 0101 1011 0101 1001 1001 0111 1000 0101) // 高地址 <-----------------------------低地址
采用大端程序应该是:
typedef struct big_endian { int A:1; // 低字节数 int B:2; // | int C:3; // | int D:4; // | int E:5; // | int F:6; // V int G:11; // 高字节数 }big_endian; int main() { big_endian BigEndian; BigEndian.A = 0x00;//0b0 低字节数 BigEndian.B = 0x02;//0b10 | BigEndian.C = 0x06;//0b110 | BigEndian.D = 0x0D;//0b1101 | BigEndian.E = 0x0C;//0b01100 | BigEndian.F = 0x32;//0b110010 V BigEndian.G = 0x0785;//0b11110000101 高字节数 printf("0x%x",BigEndian); return 0; } //输出 0x5B599785 (即 0101 1011 0101 1001 1001 0111 1000 0101) // 高地址 <-----------------------------低地址
对于小端,低字节数储存在低地址中,高字节数储存在高地址中;
对于大端,低字节数储存在高地址中,高字节数储存在低地址中。
5、位域字段不能是类的静态成员
6、总结
如果上面2、3说的不够明白的话,可以总结为位域对齐,具体规则如下:
- 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止。
- 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍。
- 如果相邻的两个位域字段的类型不同,则各个编译器的具体实现有差异,VC6采取不压缩方式,GCC和Dev-C++都采用压缩方式。
- 整个结构体的总大小为最宽基本类型成员大小的整数倍。
- 如果位域字段之间穿插着非位域字段,则不进行压缩。(不针对所有的编译器)
三、使用注意
1、位域不能取地址
例如:
typedef struct bs
{
unsigned char a:4;
unsigned char :0;
unsigned char b:4;
}Bits;
void test(void)
{
Bits bit = {0};
bit.b = 4;
bit.a = 3;
printf("a:%d, b:%d, &a:%d.\n", bit.a, bit.b, &bit.a);/* !!!这里会报错!!! */
//error: cannot take address of bit-field ...
printf("sizeof(bit):%d.\n", sizeof(bit));
}
原因是 Bit-fields成员(通常)小于指针允许的粒度,这是chars的粒度(通过char的定义,顺便说一下,它至少要求8位长)。因此,常规指针不会削减它。
此外,还不清楚指向位域成员的指针类型是什么,因为要存储/检索这样的成员,编译器必须确切地知道它在位域中的位置(并且没有"常规"指针类型可以携带此类信息)。
最后,它几乎不是一个要求的功能(首先不会经常看到位域)。 Bit-fields用于紧凑地存储信息或构建标志的打包表示(例如写入硬件端口),很少需要指向它们的单个字段的指针。
由于所有这些原因,该标准表明位域成员不可寻址。
2、由于大小端可能导致的问题
例如:
typedef struct bs
{
unsigned char a:2;
unsigned char b:3;
unsigned char c:2;
unsigned char d:1;
}Bits;
void test(void)
{
Bits bit = {0};
char result = 3;
memcpy(&bit, &result, 1);
printf("a:%d, b:%d, c:%d, d:%d.\n", bit.a, bit.b, bit.c, bit.d);
printf("sizeof(bit):%d.\n", sizeof(bit));
}
前面我们说过,位域字段在内存中的位置是按照从低位向高位的顺序放置的。但也存在位域只有一个字节(8bit)内的情况,在这种情况下,大小端读取一个数值是没有区别的,但位域里字段在内存中的位置则与之前所讲的会有所不同。小端:从低位开始排。大端:从高位开始排。
接下来我们看如何解析这个字节:
在little-endian(小端)机器上,result 字节中各个比特的存放排列如下所示
================高位<=======低位
d c b a00000011
高地址<=======低地址
=================
对应到四个位域,得
a=3,b=0,c=0,d=0
在big-endian(大端)机器上,result字节中各个比特的存放排列如下所示===================
高位=======>低位
a b c d00000011
高地址<=======低地址
===================
对应到四个位域,得
a=0,b=0,c=1,d=1
由以上例子可以看出,在使用位域时,要注意CPU的大小端字节序问题。
一定要先搞清楚大小端才能正确使用位域。