人无完人,持之以恒,方能见真我!!!
共同进步!!
一、什么是位段
在上一篇博客中,我们学习了结构体,而位段的声明和结构是类似的,它们有两个不同之处,如下:
- 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型
- 位段的成员名后边有⼀个冒号和⼀个数字
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
位段位段,那么变量后面的数字有没有可能就是二进制位,比如成员a是不是就是占据2个二进制位呢?
如果它们都按整型算,那么应该是16个字节,现在却是8个字节,但是出了一些意外,原本按二进制位算出来应该是47位,也就是6个字节,为什么这里是8个字节
那就让我们继续往下面学习
二、位段的内存分配
1.位段内存分配规则
位段的使用有以下三个需要注意的地方:
- 位段的成员可以是 int 、unsigned int 、signed int 或者是 char 等类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的,比如是整型
- int时就一次性开辟4个字节,是字符型char就一次型开辟1个字节
当开辟的空间不足时,才再次开辟对应的空间,不会一次性就开辟完所有空间,而是一个一个开辟
老规矩,看不懂,我们直接上栗子:
练习1
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}
//空间是如何开辟的?
首先第一个成员是char类型,所以会开辟一个字节的空间,然后把a放进去,那么a这三个比特位到底怎么存放呢?从左边还是从右边,我们似乎不知道
位段从开辟空间的左边还是右边开始使用,这是C语言标准未规定的,也就是说可能有的编译器实现时,从左往右开始使用,而有的编译器在实现时,又是从右往左开始使用
我们当前的编译器是VS2022,这个编译器在使用位段空间时,就是采用从右往左开始使用,那么按照这个逻辑我们看看能不能算出来位段S的最终大小
所以前面两个成员 a b 放入位段时,应该是这个样子:
接着我们来看第三个成员c,它占用5个比特位,但是现在开辟的1个字节的空间不够用了,所以现在会像第2点说的,空间不够时再次开辟对应的空间,而这里的c是char类型,所以会再次创建一个字节,如图:
那么此时问题又来了,我们的c是会继续使用第一个字节剩下的那个比特位,再加上第二个字节的4个比特位这种方式,还是会直接浪费掉第一个字节剩下的比特位,直接从右向左存放c
这也是C语言未定义的,所以有的编译器可能会使用那剩余的比特位,而有的编译器不会使用,这完全取决于编译器
所以现在我们可以按两种思路分别看看位段S算出来的大小,然后再运行代码,看当前VS采用的是哪种方法
当不使用剩余的那个比特位:此时c的存放应该是这样的:
存放完c之后,我们来看看最后一个成员d,占4个比特位,很明显第二个字节已经不够用了,并且由于我们这种情况不会利用剩余的空间,所以还要继续开辟空间,这里d也是char,所以还是开辟一个字节的空间,然后把d放进去,如图:
说明VS中很可能就不会使用剩余的比特位,将它浪费掉,为了进一步验证我们的想法,我们可以对这个位段赋值,如下:
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
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;
}
现在我们开始存放这些数据,首先是存放a,10的二进制是1010,但是成员a只有3个比特位,所以会产生截断,最后变成010,将1截断了,我们来通过画图看看a现在是什么样子:
接下来我们来看成员b,它存放的是12,对应的二进制是1100,因为b就是占用4个比特位,所以可以刚好存放完
接下来我们来看c,它存放的是3,对应的二进制是11,由于c占用5个比特位,所以我们补0变成5位:00011
最后来看d,它存放的是4,对应的二进制是100,由于d占用4位,所以补齐4位为:0100
由于我们最开始将位段s初始化为了0,也就是原本位段s中存储的是全0,而我们已经填写的位置就是在全0的基础上进行的修改,现在其它的位置没有受到影响,就还是初始化时的0,如下:
由于调试中的内存窗口采用16进制显示,所以我们先把这三个字节的二进制转为十六进制
可以看到最后内存中的结果和我们分析的一模一样,随后我们也可以得出最后的结论:VS中存放位段时,是从右往左存放,当空间不够时,会开辟新的空间,并且不会使用旧空间中的剩余比特位
那么是不是所有编译器都是这样呢?这个就不一定了,因为这些都是C语言标准未规定的,随着我们学习的深入,我们就会慢慢地了解到这些东西的本质的
练习2
#include <stdio.h>
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
printf("%zd\n", sizeof(struct A));
return 0;
}
很明显,前三个成员并不够32个字节,所以我们直接放,但是第四个成员,它占据30个比特位,而现在只剩下了32-2-5-10=15个比特位,很明显不够用了,所以我们还要继续开辟一个整型空间来存放d,如图:
所以最后VS算出来应该就是2个整型的大小,也就是8个字节,最后我们再来看一次代码运行结果:
三、位段的跨平台问题
位段虽然很节省空间,但是它却存在很大的跨平台问题,可移植性很低,因为在C语言标准中,位段的很多东西是没有规定的,导致各种编译器在实现它的时候,采用了不同的方法,我们现在来看看具体它的问题在哪里:
- int 位段被当成有符号数还是⽆符号数是不确定的,也就是说可能在有的编译器是有符号数,有的是无符号数,那么在把int位段当作有符号数的编译器上写的代码,可能就不能在把int位段当作无符号数的编译器上运行,或者运行后有bug存在,等等问题
- 位段中最⼤位的数⽬不能确定,因为32位机器的int是2个字节,也就是16个比特位,64位机器的int是4个字节,也就是32个比特位,如果把一个int位段成员写成27个比特位,那么它在32位上的机器就会出问题
- 位段中的成员在内存中从左向右分配,还是从右向左分配,C语言标准没有定义,所以不同编译器就有不同的实现方法,而VS就是采用从右向左的方式,其它有的编译器可能就是从左向右分配空间,所以在不同编译器运行相同的位段结果也可能不同
- 当⼀个结构包含两个位段,第⼆个位段成员比较,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的,像VS就会舍弃那些剩余的位,直接使用新的空间
所以综上4个理由,位段在使用的时候不一定能跨平台,也就是可移植性很低,但是相比于结构体,它能节省很多空间,我们继续学习就知道了
四、位段的应用
位段的好处就是可以节省很多空间,它可以应用在对空间有严格要求的地方,我们现在举一个例子来进行说明:
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,如果使用整型就会浪费空间
这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的
我们假设网络就是高速公路,而我们的ip数据包就是一辆辆的车,我们来比较一下它们不同大小的区别:
很明显,当我们的ip数据包越大越容易造成网络拥堵,所以使用位段来节省这些空间是很有必要的,这就是位段的应用之一
五、位段使用的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,因为内存中的地址都是以字节为单位,⼀个字节内部的bit位是没有地址的
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员
错误示范:
#include <stdio.h>
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct A sa = { 0 };
scanf("%d", &sa.b);
}
由于这里的b只占据了5个比特的空间,而地址都是以字节为单位的,所以这里的b并没有地址,使用&b就是错误的写法
正确的写法:
#include <stdio.h>
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
int b = 0;
scanf("%d", &b);
sa.b = b;
return 0;
}
这里我们重新创建了一个整型变量b,这个时候就可以对它取地址了,然后用户对b输入后,再录入我们位段的成员b,这样就可以使用位段存储用户输入的数据了
到这里呢,我们的结构体实现位段也算是结束了,例子是举不完的,还是要通过练习和自己动手才能把知识真正的记在我们的脑子里面!!!!