(1)基础
(1.1)背景
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。
(1.2)定义
所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
(1.3)格式
位域定义与结构定义相仿,其形式为:
struct 位域结构名
{
类型说明符 位域名:位域长度(位宽)
......
};
比如:
struct bs
{
int a:8;
int b:2;
int c:6;
};
(2)存储
(2.1)结构体的存储规则
(2.1.1)结构体的对齐规则:
1)结构体的每一个成员变量的首地址要能被自身类型所占的内存大小所整除;
2)结构体的大小要是最长基本类型成员变量类型的整数倍;
即:结构体遵循对齐原则,以最长的成员变量类型的长度对齐。
比如:
struct A
{
char ch1;
char ch2;
int a;
char ch3;
}a;
printf ("sizeof(a) = %d\n",sizeof(a));
成员大小为12;
为什么要对齐呢?因为以取int型数据,32位系统是以4字节取数据的,如果不对齐,那么可能取一个int型的数据,系统要取两次,这影响了效率。当然,内存对齐提高了运行效率,但也牺牲了一部分空间(有空隙),这就是鱼和熊掌不可兼得。
(2.1.2)__packed__设置结构体对齐系数
struct pp2_tlv_ns {
uint8_t type;
uint8_t len_hi;
uint8_t len_lo;
uint8_t padding;
uint32_t vni;
uint32_t vlb;
} __attribute__((__packed__));
_attribute__((__packed__)) 按照实际的结构体的大小来算,不考虑对齐;
(2.1.3)设置全局的对齐系数
每个特定平台上的编译器都有自己默认的“对齐系数”,我们可以通过预处理指令#pragma pack(n), n=1, 2, 4, 8, 16...来改变这一系数,这个 n 就是对齐系数
1)数据成员对齐规则:
结构(struct)或联合(union)的数据成员,第一个数据成员放在 offset 为 0 的地方,以后的每个数据成员的对齐按照#pragma pack(n)指定的 n 值和该数据成员本身的长度 len = sizeof(type) 中,较小的那个进行,如果没有显示指定n值,则以len为准,进行对齐
2)结构/联合整体对齐规则:在数据成员对齐完成之后,结构/联合本身也要对齐,对齐按照#pragma pack(n)指定的n值和该结构/联合最大数据成员长度max_len_of_members中,较小的那个进行,如果没有显示指定n值,则以max_len_of_members为准,进行对齐
结合1、2可推断:当n值均超过(或等于)所有数据成员的长度时,这个n值的大小将不产生任何效果;
范例:
struct{
char a;
double b;
int c;
} test;
结构体的长度就是 24 个字节;
#pragma pack(4)
struct{
char a;
double b;
int c;
} test;
对于成员a,对齐数为 1,因为 1 小于 4,放在偏移量为 0 的位置上,然后长度变为 1
对于成员b,对齐数为 4,因为 4 小于 8,放在偏移量为 4 的位置上,长度变为 4 + 8 = 12
对于成员c,对齐数为 4,两个数相等,放在偏移量为 12 的位置上,长度变为 12 + 4 = 16
最后是结构体,最大的成员长度 8,大于 4,所以取 4,刚好整除,所以就是 16 字节
(2.2)位域的存储规则
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3)位域的宽度不能超过它所依附的数据类型的长度。
通俗的讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。
4)只有有限的几种数据类型可以用于位域。
在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
即整体结构体的大小是max{最宽位域数据类型,非位域数据类型}的整数倍;
6)不可以获取位域成员的地址
位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&
获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
7) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,GCC采取压缩方式;
8) 如果位域字段之间穿插着非位域字段,则不进行压缩;
注意:4 ,5跟编译器有较大的关系,使用时要慎重,尽量避免。
(2.3)无名位域
位域成员可以没有名称,只给出数据类型和位宽,如下所示
struct bs{
int m: 12;
int : 20; //该位域成员不能使用
int n: 4;
};
作用:
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。
上面的例子中,如果没有位宽为 20 的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为 4;有了这 20 位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为 8。
(3)位域的使用
位域的使用和结构成员的使用相同,其一般形式为:
结构体变量名.位域名
位域允许用各种格式输出。
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;
pbit->a=0;
pbit->b&=3;
pbit->c|=1;
printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);
}
(4)范例
(4.1)范例一
struct test1
{
char a:1;
char :2;
long b:3;
char c:2;
};
int len = sizeof(test1);
对于上述例子,len的值应该是12.解释如下:
首先以最长的类型位宽做为偏移量,最长的是long型,占4B,所以不同类型之间应该是4个字节的偏移,即test1应该是4字节的整数倍。
char a:1; //用一个字节去存储
char :2; //空域。因为与前面的a的类型相同,而两个位域的位宽相加仍然少于8位,所以依然用1个字节表示
long b:3; //long类型的位宽是4个字节,与前面的char类型不同,所以b与a之间偏移4个字节,它们之间自动补充3个字节
char c:2; //因为c与b又不同型,以test1中的最长的long类型的位宽进行偏移,所以虽然char只用1个字节就够了
//但依然要占4个字节。
总共是12字节。
(4.1)范例二
struct s1
{
int i: 8;
int j: 4;
int a: 3;
double b;
};
struct s2
{
int i: 8;
int j: 4;
double b;
int a:3;
};
printf("sizeof(s1)= %d\n", sizeof(s1));
printf("sizeof(s2)= %d\n", sizeof(s2));
result: 16, 24
(4.3)范例三
int main(){
struct bs{
unsigned m: 6;
unsigned n: 12;
unsigned p: 4;
};
printf("%d\n", sizeof(struct bs));
return 0;
}
运行结果: 4
m、n、p 的类型都是 unsigned int,sizeof 的结果为 4 个字节(Byte),也即 32 个位(Bit)。m、n、p 的位宽之和为 6+12+4 = 22,小于 32,所以它们会挨着存储,中间没有缝隙。
如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。
(4.4)范例四
相邻成员的类型不同:
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,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。
(4.5)范例五
如果成员之间穿插着非位域成员,那么不会进行压缩;
struct bs{
unsigned m: 12;
unsigned ch;
unsigned p: 4;
};
在各个编译器下 sizeof 的结果都是 12。