位段是结构体的另一种功能,有结构体的地方就能使用位段,位段能够准确分配变量所占大小,使用巧妙的话,可以节省很多空间,但是适用的变量类型只能是整型家族的,比如int 、char等类型。
目录
位段的声明如下:
- struct 结构体名
- {
- 位段成员类型 位段成员名: 分配的内存大小 ;
- }
-
- //举例
- struct S
- {
- int a: 2; //分配 2bit 的内存大小(2 bit是站在二进制的角度来分配的)
- int b: 5; //分配 5bit 的内存大小(下同)
- int c: 3;
- int d: 4;
- }
以成员 _a 为例,_a 是 int 类型,本该分配的是 4个字节(4 byte = 32 bit),但是实际上, _a 只需要 2 bit 就够了,为了节省空间,我们就通过位段声明只给 _a 分配了 2 bit。
位段初始化的方式和结构体是一样的,但是需要注意的是,每一个成员都分配了固定大小的内存,此时就会满足一个原则 “溢出就截短,不够就补全”。下面来解释一下。
以第一个成员为例,成员 a 被分配了 2 个bit,站在二进制的角度就是分配了 00,而初始化的时候,11 的二进制数是 1011,超出了2bit,因此实际分配给 a 的只有 11,高两位会被舍弃。

以第三个成员为例,成员 c 被分配了 3 bit,站在二进制的角度就是分配了 000,初始化的值为3,对应的二进制数为 11,少了一位,此时就需要在最开始补全一位,默认补 0。

- //初始化
- struct S s = {0};
- s.a = 11; //被截短,a 分配了2 bit,取值范围为 0~3
- s.b = 12;
- s.c = 3; //要补全,c 分配了3 bit,取值范围为 0~7
- s.d = 4;
位段成员必须是属于整型家族类型,位段在内存上的开辟方式是以 4 个字节 或者 1个字节的方式来开辟的。根据成员类型判断,如果是int类型,那么就以4字节开辟;如果是char类型,则以1字节开辟。
以下面这个位段为例,我们通过逐个给位段成员分配空间来了解 位段开辟空间的方式。
- //声明
- struct S
- {
- char _a: 3; //分配3bit
- char _b: 4; //分配4bit
- char _c: 5;
- char _d: 4;
- }
第一个成员是char类型,最开始开辟空间的时候,分配的是 1个字节(即8 bit),第一个成员被分配了 3 bit。这里的占据方式没有固定的标准,会随编译器的不同而不同。假设这里就从低位开始排。

第二个成员分配了4 bit,我们可以继续在 _a 的前面占据 4 bit。

第三个成员分配了 5bit,很显然,剩下的位置不够放了,这个时候 编译器就会再给我们 开辟 1个字节(8 bit)的空间,这就解释了最开始的那句话 “位段在内存上的开辟方式是以 4 个字节 或者 1个字节的方式来开辟的”,每当位置不够的时候,根据类型分配相应的空间。
新开辟的空间就往后排,既然上一段空间无法放下第三个成员,我们就把第三个成员放到新开辟的空间上。

第四个成员分配了 4bit,很显然,上一段空间又不够放了,所以我们舍弃剩余的空间,重新开辟一个新的空间,第四个成员是 char 类型就开辟 8 bit的空间,然后在新开辟的空间上占据 4 bit。

初始化阶段需要注意最开始说的规则,溢出就阶段,缺少就补全。
- //初始化
- struct S s = {0};
- s._a = 10;
- s._b = 12;
- s._c = 3;
- s._d = 4;
第一个成员是 _a ,初始化的值是 10(对应的二进制数为1010),但是 _a 只分配了3bit,因此需要截短,保留低三位,舍弃最高位。

第二个成员是 _b,初始化的值是 12(对应的二进制数为 1100),_b正好分配了 4bit,可以直接填进去。

第三个成员是 _c,初始化的值是 3(对应的二进制数为 11),_c分配了 5bit,需要补全三位,即变成00011。

第四个成员是 _d,初始化的值是4(对应的二进制数为 100),_d分配了4bit,补全一位变成 0100。

现在四个位段成员已经全部放置完,我们将上面这串二进制数四个为一组计算,因此位段成员在内存里的排列为 62 03 04

- //声明
- 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;
- }
下面我们采用调试的方式来看一下内存里的排列方式,和我们上面逐步存放每一个成员的结果一样。

我们在最开始说过,位段必须是整型家族的一员,int作为整型家族的一员,是看作无符号整型还是有符号整型,这个并没有做出明确的规定。
一个位段成员所分配的 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;
- }
我们在分配第一个成员 _a 的内存的时候,存在这么一个问题,我们分配的时候,是从左向右使用,还是从右向左使用?这个并没有给出明确的标准,但是从我们最后的结果来看,VS编译器选择的是从右向左使用。

我们在给前两个成员开辟空间以后,第一个空间还剩下 1bit,但是无法容纳第三个成员,此时第三个成员是把第一个空间填满再放到新开辟的空间,还是直接全部放到新的空间,这个没有明确的规定。VS编译器选择的是直接全部放到新的空间。
最典型的应用就是 UDP数据包,我们在QQ、微信发送的消息在网络中不是裸奔的,而是以数据包的形式展现的。
网络传输就像是高速公路,车流量越小时就很通畅,车流量较大时就很堵,网络传输也是一样,一个端口号明明只要 16 bit,我们却整了个 32 bit,我们在传输的时候就要多传输16bit的无用信息,同时网络中可能会存在上万个数据包,每个都多出 16 bit,这很显然会降低网络的传输速度。
像下面的UDP数据包,如果你这样看着不是特别舒服,可以看第二张图,我们可以发现,没有任何一个bit被舍弃,反倒得到了充分使用。


跟结构体相比,位段也能达到存放变量的效果,而且比结构体更节省空间,但是存在跨平台的问题。