1.13 位域

在存储信息的时候,我们可能并不需要占用一个完整的字节,而只需占一个或几个二进制位,如要存储一个八进制数据,只需要3个二进制位就够了。为了节省存储空间,C语言提供了位域这种数据结构。所谓位域,就是把存储空间中的二进制位划分为几个不同的区域,并说明每个区域的位数,每个域有一个域名,允许在程序中按域名进行操作。定义位域的一般形式为:


struct

位域结构名{

类型说明符 位域名:位域长度;

……

类型说明符 位域名:位域长度;

};


位域结构名同样要遵循标识符的命名规则。位域变量的定义与之前讲解的结构体等非常类似,有以下三种方法。


struct 位域结构名{

类型说明符 位域名:位域长度;

……

类型说明符 位域名:位域长度;

}位域变量名1,位域变量名2……;


其中,位域结构名可以省略掉,直接定义位域变量,如:


struct{

类型说明符 位域名:位域长度;

……

类型说明符 位域名:位域长度;

}位域变量名1,位域变量名2……;


还可以先定义位域类型,再定义位域变量名,如:


struct位域结构名{

类型说明符位域名:位域长度;

……

类型说明符位域名:位域长度;

};

struct 位域结构名 位域变量名1,位域变量名2……;


读者可以根据自己的实际情况来决定使用哪种方式定义位域变量。下面通过代码对位域加以分析。


include<stdio.h>

struct_data

{

char a:6;

char b:2;

char c:7;

}data;

void main()

{

printf("位域变量data起始地址为:%d\n",&data);

printf("位域变量data占用内存大小为:%d字节\n",sizeof(data));

return;

}


运行结果:


位域变量data起始地址为:4233496

位域变量data占用内存大小为:2字节


我们通过图1-12说明位域变量data的内存结构。

1.13 位域 - 图1

图 1-12 位域变量data的内存结构(1)

在图1-12的内存结构中,位域变量data只占用2个字节。当相邻位域的类型相同时,如果其位宽之和小于该类型所占用的位宽大小,那么后面的位域紧邻前面的位域存储,直到不能容纳为止;如果位宽之和大于类型所占用的位宽大小,那么就从下一个存储单元开始存放。我们适当修改上面的代码来看看位宽之和大于类型所占用的位宽大小的情形。


include<stdio.h>

struct_data

{

char a:6;

char b:4;

char c:7;

}data;

void main()

{

printf("位域变量data起始地址为:%d\n",&data);

printf("位域变量data占用内存大小为:%d字节\n",sizeof(data));

return;

}


运行结果:


位域变量data起始地址为:4233496

位域变量data占用内存大小为:3字节


再通过图1-13来说明此时data的内存结构。

1.13 位域 - 图2

图 1-13 位域变量data的内存结构(2)

如果相邻位域的类型不同,不同编译器的处理方式可能有所不同,在此以VC++6.0为准进行讲解。VC++6.0在进行编译的时候,不同类型的位域存放在不同的位域类型字节中,如:


include<stdio.h>

struct_data

{

char a:6;

int b:22;

char c:7;

}data;

void main()

{

printf("位域变量data起始地址为:%d\n",&data);

printf("位域变量data占用内存大小为:%d字节\n",sizeof(data));

return;

}


运行结果:


位域变量data起始地址为:4233496

位域变量data占用内存大小为:12字节


我们通过图1-14来说明data的内存结构。

1.13 位域 - 图3

图 1-14 位域变量data的内存结构(3)

在图1-14中我们发现,默认情况下,位域结构中的字节对齐方式由其中占用字节数最大的类型所决定。在前面定义的位域中,占用内存最大的是int型,占用4字节,所以使用4字节对齐。首先从起始地址4233496处开始使用6个位域的长度来存储位域a,由于位域a和位域b为不同类型,所以不能存储在同一个字节当中,寻找下一个起始地址来存储位域b,存储位域b时要求地址的偏移量(这里的偏移量为成员起始地址相对于位域变量的起始地址,也就是相对于第一个成员的起始地址)必须是所使用的字节对齐方式和自身类型所占用字节数这两者中最小值的整数倍,这里为4字节对齐,而int变量所占用的内存大小也为4字节,即偏移量必须为4的整数倍,由此可知域b的起始地址为4233500。由于接下来的位域c是char型,与位域b不同,所以不能在int型变量所占用的存储空间中存放位域c,存储位域c从起始地址4233504开始,因为是4字节对齐,要求最终位域结构所占用的存储空间必须是4的整数倍,所以位域最终占用了12字节大小的存储空间。

适当修改上面的代码,再来看看运行结果。


include<stdio.h>

struct_data

{

int b:22;

char a:6;

char c:7;

}data;

void main()

{

printf("位域变量data起始地址为:%d\n",&data);

printf("位域变量data占用内存大小为:%d字节\n",sizeof(data));

return;

}


运行结果:


位域变量data起始地址为:4233496

位域变量data占用内存大小为:8字节


我们发现此时位域结构所占用的内存空间变小了,变为了8字节。我们仅仅交换了位域a和位域b的位置,就导致所占用的内存空间发生了变化,这是为什么呢?首先从起始地址4233496处开始使用22个位域的长度来存储位域b,因为接下来的位域是char型,所以必须存储在int型所占内存单元之外,因为位域a是char型,占用1字节,而采用的是4字节对齐,所以只需要偏移量是1的整数倍,也就是可以在接下来的地址4233500所指向的存储单元存储位域a。接下来的位域c也是char型,由于位域a和位域c两者的位宽之和为13,大于char型所占用的位宽8,所以要使用接下来的地址4233501所指向的存储单元存储位域c,由于是4字节对齐,因此最终所占用的内存大小必须是4的整数倍,此时位域结构占用了8字节。

上面都是使用默认的字节对齐方式,接下来通过“#pragma pack(2)”来指定采用2字节对齐。


include<stdio.h>

pragma pack(2)

struct_data

{

int a:16;

char b:4;

char c:6;

}data;

void main()

{

printf("位域变量data起始地址为:%d\n",&data);

printf("位域变量data占用内存大小为:%d字节\n",sizeof(data));

return;

}


运行结果:


位域变量data起始地址为:4233496

位域变量data占用内存大小为:6字节


此时,data的内存结构如图1-15所示。

我们在代码中使用了一句“#pragma pack(2)”来指定采用2字节对齐方式,与前面的代码最大的区别是,此时位域结构所占用的内存空间必须是2的整数倍,而不是4的整数倍,所以此时所占用的内存大小为6。

1.13 位域 - 图4

图 1-15 位域变量data的内存结构(4)

看完上面的讲解,细心的读者会发现一个问题,对于那些没有使用的位域段,编译器是怎么处理的呢?我们通过一段代码来分析编译器对没有使用的位域段的处理方法。


include<stdio.h>

pragma pack(2)

struct_data

{

int a:16;

unsigned char b:5;

char c:5;

}data;

void main()

{

intp=(int)&data;

printf("位域结构的起始地址为:%d\n\n",p);

data.a=2;

printf("整型指针p所指向的单元存储的值为:%d\n",*p);

printf("位域a的值为:%d\n",data.a);

charp1=(char)(p+1);

data.b=18;

printf("\n字符指针p1所指向的单元存储的值为:%d\n",*p1);

printf("位域b的值为:%d\n",data.b);

data.c=255;

char*p2;

p2=p1+1;

printf("\n字符指针p2所指向的单元存储的值为:%d\n",*p2);

printf("位域c的值为:%d\n",data.c);

return;

}


运行结果:


位域结构的起始地址为:4233624

整型指针p所指向的单元存储的值为:2

位域a的值为:2

字符指针p1所指向的单元存储的值为:18

位域b的值为:18

字符指针p2所指向的单元存储的值为:31

位域c的值为:-1


在分析代码前,我们先来看看data的内存结构,如图1-16所示。

1.13 位域 - 图5

图 1-16 位域变量data的内存结构(5)

&data为data位域结构的起始地址,将其强制转换为int型指针,并赋值给p,所以p的值就是data位域的起始地址,即4233624,p指针指向的就是以4233624为起始地址的连续4个字节的内存单元;接下来执行“charp1=(char)(p+1);”使p1的值为4233628,p1就指向地址为4233628的内存单元;执行“p2=p1+1;”使p2的值为4233629,char型指针指向地址为4233629的内存单元。我们发现,*p的值和位域a的值相同。由此可以看出,VC++6.0在编译的时候,对于那些没有使用的位域段,编译器对其进行填充0的处理。看看位域c的运行结果,我们发现输出与输入不相符,这是因为在编译的过程中对char型位域默认执行有符号处理,所以输出值为-1,而对位域b指定了无符号的处理方式,所以输出与输入完全一致。