前言
上一篇文章,我们一起学习了结构体,那学完了结构体,就一定得讲讲结构体实现位段的能力。
这篇文章,我们再来一起学习一个新知识——位段。
一起来学习吧!!!
1.什么是位段
首先,我们一起来了解一下什么是位段。
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
位段和结构体其实是非常相似的,但是有两个不同点:
1. 位段的成员必须是 char、int、unsigned int 或signed int 。
2. 位段的成员名后边有一个冒号和一个数字。
举个例子:
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; };
那里面的冒号和数字又表示什么呢?
首先我们要明白位段中的这个“位”字其实指的是二进制位。
我们知道一个二进制位就是1个比特位。
所以,A中int _a : 2;其实表示的就是
_a的大小是2bit;
同理:
_b的大小是5bit
_c的大小是10bit
_d的大小是30bit
那我们再来思考一个问题:
位段A的大小应该是多少呢?
是30+10+5+2=47个bit吗,但我们知道sizeof计算出来的是字节数啊,所以会给它分配6个字节=48个比特位吗。
我们验证一下:
是8个字节哎,为什么呢?
别着急,我们接着往下看。
2.位段的内存分配
我们已经知道:
位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型。
那位段所需要的空间是怎么来开辟的呢?
位段的空间上是按照需要以每次4个字节( int )或者1个字节( char )的方式来开辟的。
什么意思呢,解释一下:
就是说,如果位段的成员全部是整型的(位段成员一般都是同类型的),那上去就先给这个位段开辟4个字节的空间,如果不够用,放不下所有的成员,那就再开辟4个字节的空间,还不够用,继续开辟,以此类推。
如果成员全部是char类型的,那就一次开辟1个字节的空间,直至放得下所有成员。
好,那我想现在大家就明白为什么上面位段struct A的大小是8个字节了。
由于A的成员都是整型(int ),所以一次给A分配4个字节。
4个字节是32给比特位,A的前3个成员_a、_b、_c占了17个bit,32-17还剩15bit,但是A的第四个成员_d大小是30bit,而15<30不够。
怎么办?
再分配4个字节,这下就能放下_d,因此,struct A的大小是4+4=8个字节。
那现在又有一个问题:
对于刚才讨论过的struct A来说,上来先给它分配了4个字节及32比特位,前3个成员占了17个bit,32-17还剩15bit,然后这15bit留给第四个成员_d不够用,所以又开辟4个字节。
那这15bit以及新开辟的4个字节的空间是怎样给第四个成员_d分配的,有两种情况:
_d的大小是30bit,它先使用了前面剩下的15bit,然后又使用了新开辟的4个字节中的15bit。
前面剩下的15bit,就直接浪费掉不用了,_d直接使用新开辟的4个字节中的30个bit。
那到底采用的是哪种呢?其实C语言的语法也并没有做出明确的规定:
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
3.验证vs环境下位段成员如何分配内存空间
虽然是不确定的,但是我们可以来验证一下,在我们当前使用的环境(vs2022)下,位段开辟的空间是怎么分配给每个成员的。
接下来,我们就通过一个实例来探究一下:
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s1 = { 0 }; return 0; }
我们先来思考一下,s1 的大小是多少,并猜测一下这些空间是如何分配给每个成员的。
s1 的每个成员都是char类型的,所以应该每次开辟1个字节。
- 首先分配的一个字节8个比特位,是可以放得下a,b的,我们假设是这样放的:
再放c的话,只剩1个比特位,就不够了,所以需要再开辟1个字节的空间,那剩下的这1个比特位用不用呢,反正是不确定的,这里我们就假设再vs上不再使用剩下的这1个比特位了。
.那接着就要再开辟一个字节,因为还有一个成员
那vs环境下的位段变量s1的大小会是3个字节吗?
我们验证一下:
确实是3,证明我们的猜测大致是正确的。
接下来,我们再来进一步的验证一下:
int main() { struct S s1 = { 0 }; s1.a = 10; s1.b = 12; s1.c = 3; s1.d = 4; printf("%d", sizeof(s1)); return 0; }
我们先把位段变量s1 的成员都赋值为0,然后给他们重新赋一个非0值,最后,我们借助编译器观察一下,各个成员再内存中的存放是不是跟我们上面分析的一样。
现在我们通过编译器观察一下:
结果验证了在vs上位段的内存分配就是这样搞得,那在其它的平台上是不是也是这样呢?
不是的,前面已经提到了,这是不确定的,是标准未定义,在其他编译器上,可能结果就不一定是这样了。
位段是不跨平台的。
4.位段的跨平台问题
上面我们提到位段是不跨平台的,那接下来我们就来讨论讨论位段的跨平台问题。
1. int 位段被当成有符号数还是无符号数是不确定的。
什么意思呢,解释一下:
我们还拿上面的代码来说
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
位段A的成员都是int
类型的,但是我们这样直接给一个int
,它到底会被当成有符号int
还是无符号int
是不确定的。
2. 位段中最大位的数目不能确定。
什么意思呢,再把上面的代码拿过来:
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
这里面成员_d给的大小是30比特位。
但是在16位的机器上,int 类型的大小是2个字节=16个比特位,即在16位的机器上最大的位数才16位。
那如果我们把int _d:30;的代码放在16位机器上,是不是就出错了啊。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
再来看一张上面用过的图:
我们当时为什么这样放,是不是我们假设的啊,我们假设位段的成员再内存中是从右向左分配的。
为什么假设,因为这时标准未定义的,在不同的平台上可能就是不一样的。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
这个问题我们其实在上面也已经提到过了,再来看一张上面用过的图:
这张图上,当剩余的空间无法容纳后面的位段成员时,我们就是把剩余的空间舍弃了,去使用新开辟的空间来放后面的成员,而在vs上也就是这样做的。
当然它也是不确定的,在不同平台可能不同。
总结
跟结构体相比,有时候位段可以达到和结构体同样的效果,而且可以很好的节省空间,但是有跨平台的问题存在。
struct A //位段 { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; struct B //结构体 { int a; int b; int c; int d; }; int main() { printf("%d\n", sizeof(struct B)); printf("%d\n", sizeof(struct A)); return 0; }
比较一下它们的大小:
以上内容就是对结构体实现位段的一个讲解,欢迎大家指正!!!