位段是结构体的另一种功能,有结构体的地方就能使用位段,位段能够准确分配变量所占大小,使用巧妙的话,可以节省很多空间,但是适用的变量类型只能是整型家族的,比如int 、char等类型。
目录
一、位段的声明和初始化
1、位段的声明
位段的声明如下:
struct 结构体名
{
位段成员类型 位段成员名: 分配的内存大小 ;
}
//举例
struct S
{
int a: 2; //分配 2bit 的内存大小(2 bit是站在二进制的角度来分配的)
int b: 5; //分配 5bit 的内存大小(下同)
int c: 9;
int d: 4;
}
以成员 _a 为例,_a 是 int 类型,本该分配的是 4个字节(4 byte = 32 bit),但是实际上, _a 只需要 2 bit 就够了,为了节省空间,我们就通过位段声明只给 _a 分配了 2 bit。
2、位段的初始化
初始化的方式跟结构体一样,但是会根据分配的 bit位 进行略微调整。
//初始化
struct S s = {0};
s.a = 11; // 截短
s.b = 12;
s.c = 'x'; // 补全
s.d = 4;
位段初始化的方式和结构体是一样的,但是需要注意的是,每一个成员都分配了固定大小的内存,在初始化的时候,要根据分配的大小来决定是截短还是补充。
=================== 第一步 ===================
结构体的第一个成员是一个int类型,一开始会在内存里开辟 4 个字节的大小
=================== 第二步 ===================
变量 a 占 2 bit,11是int型(32 bit),此时很显然需要截短,只保留低 2 位。然后把低2位放入到上面开辟的空间中。
后面也是以此类推,具体的将在下面的案例中介绍。
二、位段内存的开辟方式
位段成员必须是属于整型家族类型,位段在内存上的开辟方式是以 4 个字节 或者 1个字节的方式来开辟的。根据成员类型判断,如果是int类型,那么就以4字节开辟;如果是char类型,则以1字节开辟。
以下面这个位段为例,我们通过逐个给位段成员分配空间来了解 位段开辟空间的方式。
1、声明阶段
//声明
struct S
{
char _a: 3; //分配3bit
char _b: 4; //分配4bit
char _c: 5;
char _d: 4;
}
(1) 第一个成员
第一个成员是char类型,最开始开辟空间的时候,分配的是 1个字节(即8 bit),第一个成员被分配了 3 bit。这里的占据方式没有固定的标准,会随编译器的不同而不同。假设这里就从低位开始排。
(2) 第二个成员
第二个成员分配了4 bit,我们可以继续在 _a 的前面占据 4 bit。
(3) 第三个成员
第三个成员分配了 5bit,很显然,剩下的位置不够放了,这个时候 编译器就会再给我们 开辟 1个字节(8 bit)的空间,这就解释了最开始的那句话 “位段在内存上的开辟方式是以 4 个字节 或者 1个字节的方式来开辟的”,每当位置不够的时候,根据类型分配相应的空间。
新开辟的空间就往后排,既然上一段空间无法放下第三个成员,我们就把第三个成员放到新开辟的空间上。
(4) 第四个成员
第四个成员分配了 4bit,很显然,上一段空间又不够放了,所以我们舍弃剩余的空间,重新开辟一个新的空间,第四个成员是 char 类型就开辟 8 bit的空间,然后在新开辟的空间上占据 4 bit。
2、初始化阶段
初始化阶段需要注意最开始说的规则,溢出就阶段,缺少就补全。
//初始化
struct S s = {0};
s._a = 10;
s._b = 12;
s._c = 3;
s._d = 4;
(1) 第一个成员
第一个成员是 _a ,初始化的值是 10(对应的二进制数为1010),但是 _a 只分配了3bit,因此需要截短,保留低三位,舍弃最高位。
(2) 第二个成员
第二个成员是 _b,初始化的值是 12(对应的二进制数为 1100),_b正好分配了 4bit,可以直接填进去。
(3) 第三个成员
第三个成员是 _c,初始化的值是 3(对应的二进制数为 11),_c分配了 5bit,需要补全三位,即变成00011。
(4) 第四个成员
第四个成员是 _d,初始化的值是4(对应的二进制数为 100),_d分配了4bit,补全一位变成 0100。
现在四个位段成员已经全部放置完,我们将上面这串二进制数四个为一组计算,因此位段成员在内存里的排列为 62 03 04
3、代码验证
//声明
struct S
{
char _a : 3; //分配3bit
char _b : 4; //分配4bit
char _c : 5;
char _d : 4;
};
int main() {
//初始化
struct S s = { 0 };
s._a = 10;
s._b = 12;
s._c = 3;
s._d = 4;
return 0;
}
下面我们采用调试的方式来看一下内存里的排列方式,和我们上面逐步存放每一个成员的结果一样。
三、位段的跨平台问题
1、int位段处理问题
我们在最开始说过,位段必须是整型家族的一员,int作为整型家族的一员,是看作无符号整型还是有符号整型,这个并没有做出明确的规定。
2、位段最大bit位的问题
一个位段成员所分配的 bit数 是不能超过自身类型限制的bit数的,一个int类型占 4字节(32bit),此时你分配30bit是没有问题的。
==》如果放到早期的 16位机器上,16位机器上的int类型占 16bit,而我们的 _d 是30 bit,此时就会有问题;
==》如果放到 64 位机器却没有问题,因为64位机器上一个int类型 也是占 4字节(32 bit),_d 的30bit没有超过32bit,所以没有问题。
struct S
{
int _d: 30;
}
3、空间使用的顺序问题
我们在分配第一个成员 _a 的内存的时候,存在这么一个问题,我们分配的时候,是从左向右使用,还是从右向左使用?这个并没有给出明确的标准,但是从我们最后的结果来看,VS编译器选择的是从右向左使用。
4、位段空间开辟存在的问题
我们在给前两个成员开辟空间以后,第一个空间还剩下 1bit,但是无法容纳第三个成员,此时第三个成员是把第一个空间填满再放到新开辟的空间,还是直接全部放到新的空间,这个没有明确的规定。VS编译器选择的是直接全部放到新的空间。
四、位段的应用
最典型的应用就是 UDP数据包,我们在QQ、微信发送的消息在网络中不是裸奔的,而是以数据包的形式展现的。
网络传输就像是高速公路,车流量越小时就很通畅,车流量较大时就很堵,网络传输也是一样,一个端口号明明只要 16 bit,我们却整了个 32 bit,我们在传输的时候就要多传输16bit的无用信息,同时网络中可能会存在上万个数据包,每个都多出 16 bit,这很显然会降低网络的传输速度。
像下面的UDP数据包,如果你这样看着不是特别舒服,可以看第二张图,我们可以发现,没有任何一个bit被舍弃,反倒得到了充分使用。
五、总结
跟结构体相比,位段也能达到存放变量的效果,而且比结构体更节省空间,但是存在跨平台的问题。