C语言结构体位域及其存储
前言
在一些应用中,比如网络协议,经常会涉及对数据的某些比特位进行操作,尽管可以使用位的相关运算,但是C语言提供了位域用以支持对一个字节的某几个位进行访问,操作起来也更加方便。本文关注于说明C语言中位域的使用及其在内存中的排列规则,尤其在大小端平台下位域存储的差异。
位域的定义与引用
位域不同于一般的结构体成员,它以位为单位来定义成员的长度,因此在结构体中定义位域时,必须要指明位域成员所需要占用的二进制位数。一个简单的定义位域的示例如下:
struct Foo {
int a: 5; // 数据类型名 变量名:二进制位数
int b: 3;
int c: 2;
};
由于位域本质上是一种特殊的结构体成员,因此一般结构体成员的引用方法同样适用于位域成员。不过,需要特别注意的是,位域成员存储是以二进制位作为单位的,而内存的最小寻址单元是字节,所以不能直接引用位域成员的地址。
匿名位域
在使用位域时,如果有需要可以选择跳过某些位不使用,其方法是在结构体类型中定义位域成员时,只指定成员占用的二进制位数,而不定义成员名。由于被跳过的这些位域成员没有名字,因此在程序中也无法进行引用。
struct Foo {
int a: 3;
int b: 4;
int : 5; // 定义匿名位域,选择性跳过部分位不使用
int c: 3;
};
特别的,我们可以根据需要定义二进制位数为0的匿名位域成员,目的是指定某些位域从一个新的内存单元开始存放。
位域的存储
结构体中位域在内存中的存储遵循以下规则:
- 若相邻的位域成员的类型相同,且其占用二进制位数未超过该类型可容纳的范围,则后面的成员紧接着前一个成员进行存储;
- 若相邻的位域成员的类型相同,但其占用二进制位数超过该类型可容纳的范围,则后面的成员从该类型占用空间之后的内存单元开始存储;
- 若相邻的位域成员的类型不同,则取决于编译器的实现。对于GCC编译器会尽量利用空闲的位对数据进行存储;
- 若位域之间定义有匿名位域成员,则匿名位域成员指定的空闲位不用于后续成员的数据存储;
说明示例1
考虑如下结构体,该结构体内的定义几乎使用到了上面提到的所有规则:
- 对于成员a、b、c再加上匿名成员占用的总内存空间并未超过unsigned short类型空间的大小,因此在unsigned short类型可容纳的范围内,这些成员可以紧挨着存放;
- 当后续存放成员d时,前一个unsigned short类型数据剩余的空间已不足以容纳d,因此选择下一个内存单元进行存放。
特别地,如果定义上述结构体中匿名成员占用的位数为0,那么对于第一个unsigned short类型数据在除被a、b占用区域的其它剩余空间都将不会被使用,则成员c需要从byte2开始进行存储。
说明示例2
现在考虑相邻位域成员类型不同的情况,定义如下结构体并画出其在内存中的布局如下(这个结果基于gcc编译器):
GCC编译器会尽可能地利用空闲的内存位进行位域成员的存放,这里有一点需要注意的是,尽管对于第一个unsigned char类型可容纳的单字节范围在存放完成员a和b后,剩余的一位已不足以存放成员c,但是GCC编译器仍然将这一位分配给了c。
位域与大小端
系统的大小端差异会同时牵涉到字节序和比特序问题,对于结构体位域这种会涉及比特位层面数据的操作,几乎需要时刻考虑平台大小端的差异。结构体内位域成员在大小端系统上的内存分配规则如下:
- 无论是大端或小端模式,位域的存储都是由内存低地址向高地址分配,即从低地址字节的低位bit开始向高地址字节的高位bit分配空间;
- 位域成员在已分配的内存区域内,按照机器定义的比特序对数据的各个bit位进行排列。即在小端模式中,位域成员的最低有效位存放在内存低bit位,最高有效位存放在内存高bit位;大端模式则相反。
程序示例
为了说明上述的规则,参考如下代码:
struct Foo2 {
unsigned short a: 1;
unsigned short b: 3;
unsigned short c: 4;
unsigned short d: 4;
unsigned short e: 4;
};
void test_foo2(void)
{
union {
struct Foo2 foo;
unsigned short s_data;
}val;
val.foo.a = 1;
val.foo.b = 3;
val.foo.c = 5;
val.foo.d = 7;
val.foo.e = 15;
printf("val is 0x%x.\n", val.s_data);
return;
}
运行上述程序:
- 在小端设备上的输出的结果为:0xf757;
- 换成大端设备,程序的运行结果为:0xb57f。
我们画出程序所定义结构体位域成员在内存中的存储布局如下所示(因为平时多数都是在使用小端机器,在此有意将字节的顺序和字节内的位顺序进行颠倒,方便对比):
为了更便于理解位域在内存中的排列规则,建议将位域成员内存空间的分配和解析分开来看:
- 第一步先考虑内存空间的分配,从上图中可以看到,不论大小端都是从内存地址的低位开始;
- 当位域成员占用空间确定之后,考虑于位域成员数据位的排布,可以看到小端系统从低bit位开始存放数据,这是符合我们预期的,而大端设备则恰恰相反,大端系统从高bit位开始存放数据,因此在大端设备中,我们需要转换下思维从内存的高位开始解析数据位。
相关参考
- 《C语言深度剖析》
- 《程序设计艺术》