本小节,我们将学习结构体最后的知识:结构体实现位段,阿森将会和你一起去学习什么是位段?位段的内存分配,VS
怎么开辟位段空间呢?位段跨平台问题,随即位段的应用,最后我们也要了解它的注意事项。文章干货满满,很容易理解,学习起来吧!
位段是C语言中结构体的一种数据类型。
位段允许在结构体中定义具有指定位数的成员,这些成员可以占用结构体变量内部的连续比特位。
位段的声明和结构是类似的,有两个不同:
位段的成员必须是int
,usigned int
或 signed int
,在C99中
位段成员的类型也可以选择其他类型。
位段的成员后边有一个冒号和一个数字,这个数字代表了该成员变量在结构体内占用的bit位数。它用来限定成员变量的范围和存储空间。。
话不多说,给铁铁上两者比较代码:
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("位段A大小=%d\n", sizeof(struct A));
printf("结构体B大小=%d\n", sizeof(struct B));
return 0;
}
首先看位段Struct A
有4
个成员,如int _a:2
这个成员中,int
是类型,_a
是变量名【变量名包含字母(大小写均可),数字(但不能以数字开头),下划线,如良好的变量名userName
,order_calculateResult
】,:2
指定该位段成员占用的bit
位数为2
个bit
,以此类推就会明白_b
,_c
,_d
的组成情况。既然知道了他的组成,那计算他的大小吧,Struct A
的大小和为47bit
(2+5+10+30=47bit
),然后用编译器运行大小为8
(这个8
意思是八个字节,也等于8*8=64
个比特位)。我们通过位段的一个成员一个成员加起来是47bit
,而编译器计算出的是8
个字节。
阿森小问:这
8
个字节是内存实际占用的吗?为什么编译器不显示47
个bit,而是64
个bit
,是不是跟结构体一样存在内存对齐呢?通过内存对齐来此应对内存的节约呢?阿森小答:没错,节省空间是没错,用的是也是同结构体一样的内存对齐的实现方式:字节对齐,不过方法不同。对于编译器来说,最小的内存单元是字节,它不会返回非整字节的bit数,因此它是按字节为单位返回,打印8个字节。位段成员总和47bit
,6
字节(48bit
)就可以了,怎么又要8
(64bit
)个字节了。通过结构体(128bit
)与位段(64bit
)对比,我们看出他的空间节省出来了,但是他不是无限制的节省空间,虽然节省了空间,但也有浪费,阿森一会讲解怎么浪费空间的。当然对于位段是要使用在特殊场景下,如在struct B
中的int _a
;假设他存储134
,267
这么大的整数那就不适合用位段,如果要存储0,1,2,3
用2bit
就可以完美的存储起来了。 0可以用00,1用01,2用10,3用11表示,而用int 存储可能需要32bit,节省了很多空间!那位段怎么实现内存分配,让47(bit)变成8(64bit)字节呢?
int
, unsigned int
,signed int
或者是 char
等类型。4
个字节( int
)或者1
个字节( char
)的⽅式来开辟的。struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
首先_a
的类型是int
,申请了4
个字节,开辟32bit
空间,_a
需要2bit
,到底是从右边开始使用,还是从左边使用这两个空间开始的,这个是不确定的,标准C语言并没有给规定,这取决于编译器,注:这不是大小端问题。假设它从右向左,分配2
个空间给_a
(绿色),然后再继续分配5
个空间给_b
(黄色),接着_c
(蓝色)说我需要10bit
,最后还剩下 15bit
,接下来_d
说我需要30个bit
,15
个bit
不够,内存说:那就再给你开辟一个整形32
个bit
吧!然后他就存储完剩下的15bit
,再存储新开辟的32bit
里分配15bit
继续存储,这是一种方式!当然也有第二种可能:剩下的我浪费掉,我不用,反正不够,那我在新开辟的空间里一些性存储完30个bit
,这是不是一种方式。对于这个剩下的15个bit
会不会使用,C语言有没有给规定,这也取决于编译器,VS
是一种实现,gcc
是一种实现,这就说明了位段有很多不确定因素,位段是不跨平台的,位段是如何开辟空间的,是严格依赖编译器的!注重可以植平台应该避免使用位段,如果要使用,应该明白其开辟空间原理,避免造成不必要的麻烦!
上代码来一起实战理解:
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 = 8;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(s));
return 0;
}
代码运行:
阿森双手把宝图奉上:
图解分析:
首先一上来给s
的成员都初始化为0
,也就是每个bit
都初始化为0
,s
里的每个成员类型都是char
,为了更好的理解他开辟的空间是什么样的?我们先开辟一个字节(8bit
(两个黑色箭头处在同一字节处)),开辟好了,a
占3
个bit
,是从2
个黑色箭头往左使用,还是从开头往右使用的呢?剩余的空间不够了,是浪费,还是不浪费呢?这样子吧!我们先假设一种方案来:1. 从右向左使用,2.如果剩余的空间不够就直接使用下一个空间,浪费掉。
开始–>:先看两个箭头指向一个字节处,
a
是10
,用二进制位表示01010
(注:在x86
环境下,整数10
二进制表示方式为0000 1010
,这里为了方便看,简写5
为就能理解了),a
要3bit
,并没有把a全部存进去,从a
取低位开始010
,接着箭头移动三
格,然后b
要4bit
,取1100
,放进去,此时8bit
只剩下1bit
,根据我们定下的规则,如果剩余的空间不够,就浪费,使用下一个。好!接下来再开辟一个字节(8bit
),黑色箭头指向下一个字节最右边,c
你要5
个bit
,好!一下子满足你,此时发现8bit
只剩下3bit
了又不够,好!编译器说:再给你在内存空间里弄一个字节(8bit
)吧,d
要4bit
,最后用了4bit
,都存完了,总共3
个字节。你可能说:有没有巧合呢?不充分吧!那阿森和你一起就调试起来看看内存和监视吧:注意:在内存窗口我们看到是
16
进制存储方式,先把我们成员存储进去的bit
进行16
进制转换,再看内存。
拓展:2进制转16进制方式:
16进制的数字每⼀位是0~9
,a ~f
的,0 ~9
,a ~ f
的数字,各⾃写成2
进制,最多有4
个2
进制位就⾜够了,
如:2进制的01101011,换成16进制:0x6b,16进制表⽰的时候前⾯加0x
因此,我们把每个字节(8bit
)划分2
段4bit
,然后再加上0x
就可以;
第一个字节是前4
位0110
–>2^0+ 2^1+ 2^1+ 2^0=6
,后4bit
为0010
–>2^0+ 2^0 +2^1 + 2^0=2
,剩下的都是同样方法,00000011
表示0x03
,00000100
表示0x04
,接下来看内存调试:
看出内存显示的确是62 03 04
,一模一样。说明我们刚刚的方案是正确,符合VS的存储方式的:在一个字节内部存储数据从右向左使用,如果剩余的空间不够,就浪费。
代码输出:
分析结果:
这里可以看出开辟了3
个字节,就可以把我们想存储的数据就存好了,如果没有位段的使用,用结构体要开辟4
个char
类型,多出来一个字节,相对来说节省了空间。
当你读到这里,你已经明白了VS
对位段的开辟是怎么样操作的,此时让我们给自己鼓个掌,送给自己,继续加油!
阿森和你再理清这3
个字节是不是一次性开辟的存储数据,还是创建完一个字节存储数据,再创建一个字节再存储数据的。
用图更容易理解:
s
是编译器一次性开辟好的,然后再存储数据,文章中为了更好的理解他的流程,所以用了一个字节开辟一个字节开辟的存储的数据!
内存调试也可以方便观察:按F10调试内存来看看,给内存输入&s,当调试s的成员进行初始化为0
时,内存显示3
个字节变红了,都为0
,后面cc
代表着还未被初始化,为随机值(经典烫烫烫),可以看出在给一个成员s
开辟内存空间时,编译器是一下子分配好的,不是开辟一个字节空间就存储数据,内存调试图在下↓
int
位段被当成有符号数还是⽆符号数是不确定的。16
位机器最⼤16
,32
位机器最⼤32
,写成27
,在16
位机器会出问题。下图是⽹络协议中,IP
数据报的格式,我们可以看到其中很多的属性只需要⼏个bit
位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。
IP
数据报(IP Datagram)是IP(Internet Protocol)网络层协议传输的数据单元。
IP
数据报报头中的许多字段,其值的范围很小,只需要使用少量比特位就可以表示,这就适合使用位段表示。
比如说4
位版本号版本号是不是给4bit
就可以了?首部长度给4bit
,服务类型给8bit
,总长度给16bit
,包括这个地方的标志位给上3bit
就可以了,那像这种是不是实现这位段的形式更好一些?
什么叫ip
数据报?简单地说一下,假设呢,你要聊天,说a
要发一个信息给b
。
假设我们的使用微信,你在微信上发了一个元旦快来啦,之后,你就一下子就发到b手机上去了吗,你只要把它扔到网络上,就发到b的手机去了,不是的。
首先发送数据时,不仅仅发送原始数据,还需要封装额外的控制信息,如版本号、长度、源地址、目的地址等,组成完整的IP数据报,这些控制字段使用位段表示,精确占用需要的比特位数,可以最大限度节省空间。源地址和目的地址决定数据报发往哪里,避免误发。
数据报大小合理,就像网络上车流量合理,可以提高传输效率(如果封装的
13
个数据都是int
好比许多大车,传输效率慢,合理位段像不同的小车高效运行传输)
小尺寸的IP
数据报更利于网络传输。因为网络传输的开销很大程度上取决于数据包的大小。
网络协议定义了数据报的格式,保证发送和接收双方都能正确理解数据内容。使用位段表示IP
报头字段,可以有效减小IP
数据报的大小,这对网络传输性能和通信效率都很有利。所以,位段就起到了一个很好的编解码方法,它可以帮助IP
数据报更高效地使用报头空间,实现报头字段的最优编码。
这也是IP
报头设计中广泛使用位段的重要原因。它可以很好地将IP
数据报大小控制在一个合理范围内。
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。
内存中每个字节分配⼀个地址,⼀个字节内部的bit
位是没有地址的。所以不能对位段的成员使⽤&
操作符,这样就不能使⽤scanf
直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
代码:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
scanf("%d", &sa._b);//这是错误的
return 0;
}
错误显示图:
正确方法:必须先将输入值存入有地址的普通变量中,然后赋值给位段成员。
例如先scanf
输入一个整数到变量b
,然后b
的某几位赋值给位段成员。
正确代码:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
这次阿森和你一起学习什么是位段? 位段的内存分配,VS怎么开辟位段空间呢?位段的跨平台问题,位段的应⽤,位段使⽤的注意事项,阿森将下一节和你一起学习联合体和枚举。
感谢你的收看,如果文章有错误,可以指出,我不胜感激,让我们一起学习交流,如果文章可以给你一个小小帮助,可以给博主点一个小小的赞,也可以点个小小的关注哦