转自:http://www.cnblogs.com/pure/archive/2013/04/22/3034818.html
几篇较全面的位域相关的文章:
http://www.uplook.cn/blog/9/93362/
C/C++位域(Bit-fields)之我见
C中的位域与大小端问题
内存对齐全攻略–涉及位域的内存对齐原则
本文主要对位域相关知识进行了一下梳理,参考如下:
C语言中的位域
史上最全的C位域总结2
C结构体之位域(位段)
C/C++中以一定区域内的位(bit)为单位来表示的数据成为位域,位域必须指明具体的数目。
位域的作用主要是节省内存资源,使数据结构更紧凑。
1. 一个位域必须存储在同一个字节中,不能跨两个字节,故位域的长度不能大于一个字节的长度。
如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct BitField { unsigned int a:4; //占用4个二进制位; unsigned int :0; //空位域,自动置0; unsigned int b:4; //占用4个二进制位,从下一个存储单元开始存放; unsigned int c:4; //占用4个二进制位; unsigned int d:5; //占用5个二进制位,剩余的4个bit不够存储4个bit的数据,从下一个存储单元开始存放; unsigned int :0; //空位域,自动置0; unsigned int e:4; //占用4个二进制位,从这个存储单元开始存放; };
2. 取地址操作符&不能应用在位域字段上;
3. 位域字段不能是类的静态成员;
4. 位域字段在内存中的位置是按照从低位向高位的顺序放置的;
struct BitField { unsigned char a:2; //最低位; unsigned char b:3; unsigned char c:3; //最高位; }; union Union { struct BitField bf; unsigned int n; }; union Union ubf; ubf.n = 0; //初始化; ubf.bf.a = 0; //二进制为: 000 ubf.bf.b = 0; //二进制为: 000 ubf.bf.c = 1; //二进制为: 001 printf("ubf.bf.n = %u\n", ubf.n);
位域中的位域字段按照从低位向高位顺序方式的顺序来看,那么,a、b、c这三个位域字段在内存中的放置情况是:
最高位是c:001,中间位是b:000,最低位是a:000;所以,这个位域结构中的8二进制内容就是: 00100000,总共8个位,其十进制格式就是32;
实际上打印出来的ubf.n值就是32;
ubf.n = 100; //二进制为: 01100100
printf("ubf.bf.a = %d, ubf.bf.b = %d, ubf.bf.c = %d\n", ubf.bf.a, ubf.bf.b, ubf.bf.c);
此时,对于位域ubf.bf来说,其位于字段仍然按照从低位向高位顺序方式的顺序放置,则,最高位是c:011,中间位是b:001,最低位是a:00;
所以,ubf.bf.a = 0; ubf.bf.b = 1; ubf.bf.c = 3;
实际上打印出来的结果也的确如此;不够存储下一个位域的4位,故设为空位域,不使用,自动置0;e从第四个字节处开始存放,占用4位;
5. 位域的对齐
1. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3.如果相邻的两个位域字段的类型不同,则各个编译器的具体实现有差异,VC6采取不压缩方式,GCC和Dev-C++都采用压缩方式;
4. 整个结构体的总大小为最宽基本类型成员大小的整数倍。
5. 如果位域字段之间穿插着非位域字段,则不进行压缩;(不针对所有的编译器)
struct BFA { unsigned char a:2; unsigned char b:3; unsigned char c:3; }; struct BFB { unsigned char a:2; unsigned char b:3; unsigned char c:3; unsigned int d:4; //多出来这个位域字段; };
sizeof(BFA)=1, sizeof(BFB)=8;
这也说明了第三点中"相邻两个位于字段类型不相同时,VC6采取不压缩的方式"
6. 当要把某个成员说明成位域时,其类型只能是int,unsigned int与signed int三者之一(说明:int类型通常代表特定机器中整数的自然长度。short类型通常为16位,long类型通常为32位,int类型可以为16位或32位.各编译器可以根据硬件特性自主选择合适的类型长度.见The C Programming Language中文 P32)。
尽管使用位域可以节省内存空间,但却增加了处理时间,在为当访问各个位域成员时需要把位域从它所在的字中分解出来或反过来把一值压缩存到位域所在的字位中.
#include#include using namespace std; struct A { int a:5; int b:3; }; int main(void) { char str[100] = "0134324324afsadfsdlfjlsdjfl"; struct A d; memcpy(&d, str, sizeof(A)); cout << d.a << endl; cout << d.b << endl; return 0; }
在32位x86机器上输出:
高位 00110100 00110011 00110001 00110000 低位 '4' '3' '1' '0' 其中d.a和d.b占用d低位一个字节(00110000),d.a : 10000, d.b : 001
解析:在默认情况下,为了方便对结构体内元素的访问和管理,当结构体内的元素长度都小于处理器的位数的时候,便以结构体里面最长的元素为对其单位,即结构体的长度一定是最长的数据元素的整数倍;如果有结构体内存长度大于处理器位数的元素,那么就以处理器的位数为对齐单元。由于是32位处理器,而且结构体中a和b元素类型均为int(也是4个字节),所以结构体的A占用内存为4个字节。
上例程序中定义了位域结构A,两个个位域为a(占用5位),b(占用3位),所以a和b总共占用了结构A一个字节(低位的一个字节)。
当程序运行到14行时,d内存分配情况:
高位 00110100 00110011 00110001 00110000 低位 '4' '3' '1' '0' 其中d.a和d.b占用d低位一个字节(00110000),d.a : 10000, d.b : 001
d.a内存中二进制表示为10000,由于d.a为有符号的整型变量,输出时要对符号位进行扩展,所以结果为-16(二进制为11111111111111111111111111110000)
d.b内存中二进制表示为001,由于d.b为有符号的整型变量,输出时要对符号位进行扩展,所以结果为1(二进制为00000000000000000000000000000001)
另一个例子,来自http://blog.chinaunix.net/uid-28697486-id-3511598.htm
#include "stdio.h" void main(int argn ,char *argv) { struct test { unsigned a:10; unsigned b:10; unsigned c:6; unsigned :2;//this two bytes can't use unsigned d:4; }data,*pData; data.a=0x177; data.b=0x111; data.c=0x7; data.d=0x8; pData=&data; printf("data.a=%x data.b= %x data.c=%x data.d=%xn",pData->a,pData->b,pData->c,pData->d);//位域可以使用指针 printf("sizeof(data)=%dn",sizeof(data)); //4 bytes ,最常用的情况 struct testLen{ char a:5; char b:5; char c:5; char d:5; char e:5; }len; printf("sizeof(len)=%dn",sizeof(len)); //5bytes 规则2 struct testLen1{ char a:5; char b:2; char d:3; char c:2; char e:7; }len1; printf("sizeof(len1) =%dn",sizeof(len1)); //3bytes 规则1 struct testLen2{ char a:2; char :3; char b:7; long d:20; //4bytes char e:4; }len2; printf("sizeof(len2)=%dn",sizeof(len2)); //12 规则3,4,5,总长为4的整数倍,2+3 占1byte,b占1bye 由于与long对其,2+3+7 占4字节,后面 d 与 e进行了优化 占一个4字节 struct testLen3{ char a:2; char :3; char b:7; long d:30; char e:4; }len3; printf("sizeof(len3)=%dn",sizeof(len3));//12 规则3,4,5,总长为4的整数倍,2+3 占1byte,b占1bye 由于与long对其,2+3+7 占4字节,后面 d占一个4字节,为了保证与long对其e独占一个4字节 }
声明 :《ARM 嵌入式系统开发--软件设计与优化》是我见过的最好的关于 ARM 软件开发的参考资料。
今天早晨趴在被窝里啃着一大堆资料,啃着,啃着,感觉不对劲了,位域是很经典的C语言用法阿,想当年刚入行的时候不知道位域还被高手嘲笑过,效率怎么可能那么低呢 ?仔细琢磨书中给出的参考代码,嘿嘿,好像有漏洞哦。
想想立刻可以对这么一部大部头的作者拍砖,这使我立刻兴奋起来,立刻起床,不用洗澡,不用刷牙,打开电脑准备拍砖 。Shit ,竟然连 ARM 编译器都没有装,咳 ,好久都不上进了。立刻翻箱倒柜的找光盘,安装,搞定编译器,从出版社拉下来对应的源代码,编译,察看,嘿嘿,果不出其然。
书中的例子一没有使用局部变量,例子二却使用了。于是编写如下新代码 :
void dostages_v3(Stages_v1 *stages_v3)
{
Stages_v1 stages = *stages_v3;
if (stages.stageA)
{
dostageA();
}
if (stages.stageB)
{
dostageB();
}
if (stages.stageC)
{
dostageC();
}
}
编译后,有如下的汇编代码 :
dostages_v3
STMFD r13!,{r4,r14}
LDR r4,[r0,#0]
CMP r4,#0
BLLT dostageA
MOV r0,r4,LSL #1
CMP r0,#0
BLLT dostageB
MOV r0,r4,LSL #2
CMP r0,#0
LDMLTFD r13!,{r4,r14}
BLT dostageC
LDMFD r13!,{r4,r14}
BX r14
************************
dostages_v2
STMFD r13!,{r4,r14}
LDR r4,[r0,#0]
TST r4,#1
BLNE dostageA
TST r4,#2
BLNE dostageB
TST r4,#4
LDMNEFD r13!,{r4,r14}
BNE dostageC
LDMFD r13!,{r4,r14}
BX r14
**********************
仅仅比例子二多了 两条 MOV r0,r4,LSL #2 语句 再察看后,嘿嘿,变量没有对其吗,而例子二是对齐的.于是有了如下的测试代码 :
typedef union bit_area_u
{
struct bit_area_s {
volatile uint32 other : 15 ;
volatile uint32 reserved : 7 ;
volatile uint32 low : 3 ;
volatile uint32 high : 5 ;
volatile uint32 enable : 1 ;
volatile uint32 clear : 1 ;
}bit;
uint32 value ;
}BIT_AREA_U;
#define STAGEA_TEST (327ul)
#define STAGEB_TEST (328ul)
#define STAGEC_TEST (329ul)
void dostages_test1(BIT_AREA_U *stages_v1)
{
BIT_AREA_U stages = *stages_v1;
if (stages.bit.other & STAGEA_TEST)
{
dostageA();
}
if (stages.bit.other & STAGEB_TEST)
{
dostageB();
}
if (stages.bit.other & STAGEC_TEST)
{
dostageC();
}
}
#define STAGEA_TEST_2 (327ul<<16)
#define STAGEB_TEST_2 (328ul<<16)
#define STAGEC_TEST_2 (329ul<<16)
/* Example 5.9 */
void dostages_test2(uint32 *stages_v2)
{
uint32 stages = *stages_v2;
if (stages & STAGEA_TEST_2)
{
dostageA();
}
if (stages & STAGEB_TEST_2)
{
dostageB();
}
if (stages & STAGEC_TEST_2)
{
dostageC();
}
}
编译后的汇编代码如下 :
dostages_test1
STMFD r13!,{r4,r14}
LDR r4,[r0,#0]
MOV r0,#0x47
ADD r0,r0,#0x100
TST r0,r4,ASR #17
BLNE dostageA
TST r4,#0x2900000
BLNE dostageB
MOV r0,#0x49
ADD r0,r0,#0x100
TST r0,r4,ASR #17
LDMNEFD r13!,{r4,r14}
BNE dostageC
LDMFD r13!,{r4,r14}
BX r14
*************************
dostages_test2
STMFD r13!,{r4,r14}
LDR r4,[r0,#0]
MOV r0,#0x470000
ADD r0,r0,#0x1000000
TST r4,r0
BLNE dostageA
TST r4,#0x1480000
BLNE dostageB
MOV r0,#0x490000
ADD r0,r0,#0x1000000
TST r4,r0
LDMNEFD r13!,{r4,r14}
BNE dostageC
LDMFD r13!,{r4,r14}
BX r14
嘿嘿,代码的效率是一样,一样的 ..................
2 ,位域赋值操作的对比代码如下 :
volatile BIT_AREA_U bit_test;
volatile uint32 int_test;
uint32 bit_area_test (void)
{
bit_test.bit.high = 327 ;
return bit_test.value;
}
uint32 bit_area_test_32 (void)
{
uint32 i = int_test;
//Clear old value ;
i &= ~0x1C ;
//Set new value ;
int_test = i | (327<<2);
return int_test;
}
编译后的汇编代码如下 :
bit_area_test
LDR r0,0xe0
LDR r1,[r0,#0]
BIC r1,r1,#0x7c
ORR r1,r1,#0x1c
STR r1,[r0,#0]
LDR r0,[r0,#0]
BX r14
********************
bit_area_test_32
LDR r0,0xe0
LDR r1,[r0,#4]
ORR r1,r1,#0x400
ORR r1,r1,#0x11c
STR r1,[r0,#4]
LDR r0,[r0,#4]
BX r14
嘿嘿,效率也是一样一样的...............
3 ,位域清除代码的对比 :
uint32 bit_area_clear_1 (void)
{
bit_test.bit.high = 0 ;
return bit_test.value;
}
uint32 bit_area_clear_2 (void)
{
//int_test &= 0x0xFFFFFF13 ;
int_test &= ~0x7C ;
return int_test;
}
bit_area_clear_1
LDR r0,0xe0
LDR r1,[r0,#0]
BIC r1,r1,#0x7c
STR r1,[r0,#0]
LDR r0,[r0,#0]
BX r14
*********************
bit_area_clear_2
LDR r0,0xe0
LDR r1,[r0,#4]
BIC r1,r1,#0x7c
STR r1,[r0,#4]
LDR r0,[r0,#4]
BX r14
嘿嘿, 效率也是一样的 ........
对于5.8节小结 :
1,124 页, 第四行汉字,"避免使用位域对提高效率是有好处的" 的说法太草率.
其实,只要保持良好的编程风格和习惯,使用位域不仅不会降低代码的效率,而且还有很好的可读性.
2,125页 ,第二行汉字,"注意编译器一共访问了包含位域的寄存器位置3次"的原因不是"因为位域存储在存储器中",而是没有跟例子二一样使用局部变量.导致编译器没有对代码进行优化.
3 126 页 ,第四行汉字,“代码量比前面使用ANSI位域的版本减少了33%”,这是不对的,使用变量后,仅仅多两行代码,如果再对参数进行对齐处理后代码量是一样的。
4 ,127页 ,反对“应避免使用位域” 的结论,原因如上。
其实我个人更喜欢位域操作,原因如下 :
1 ,具有更好的代码可读性。
2 ,只要更换位域的顺序即可适合不同公司的CPU ,代码更改少;
3 ,赋值超出了位域的宽度时,编译器可以保证,操作不会影响其他的位;
4 ,我是一个懒人, 实在懒得对着手册去数左移或者右移多少位。
好了,最后感谢Andrew ,沈建华教授给我们带来的这本好书,很值得一读 .........
附录:
1,书中源码的下载地址 www.mkp.com/companions/1558608745