• 结构体的简单介绍(4)——位段


    目录

    位段的概念:

    位段的内存分配:

    问题1:当开辟了内存后,内存中每个比特位从右向左使用?还是从左向右使用? 这个不确定。

    问题2:当前面时候,剩余的空间不足下一个成员使用的时候,剩余的空间是否使用?这个不确定。

    探究VS中的位段内存分布情况: 

    总结图:

    位段的跨平台问题:

    注意事项:

    热知识:

    问题返回:

    让我们返回最开始的例子:

    经典例题:

    解析:

    图解:

    注意:



    位段的概念:

    位段的声明和结构是类似的,有两个不同:

    1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。
    2. 位段的成员名后边有⼀个冒号和⼀个数字。 

    比如:

    1. 位段:
    2. struct A
    3. {
    4. int _a:2;
    5. int _b:5;
    6. int _c:10;
    7. int _d:30;
    8. };
    9. 结构体:
    10. struct A
    11. {
    12. int _a;
    13. int _b;
    14. int _c;
    15. int _d;
    16. };

    以上代码分别是位段和结构体的对比,可以看出位段其实是基于结构体的。

    位段的目的:

    位段的出现,是为了节省空间的,而且位段中的位其实是比特位,或者说是二进制位。

    比如:

    1. struct A
    2. {
    3. int _a:2;
    4. int _b:5;
    5. int _c:10;
    6. int _d:30;
    7. };

    上一串代码中,int_a:2; 中的2其实是表示两个比特位,也就是两位二进制。

    而整个两个比特位的本质是:当我们给一个变量赋值时,这个数值在32位比特位中其实只需要两个比特位就能表达这个值是谁,例如我们本来是要给a赋值1,但1的二进制表达是01,只需要两个比特位就能表达1,而剩下的32位就会被浪费,造成空间的损失。

     以此类推,其他成员分别占据,5个比特位,10个比特位,30个比特位。

    因为位段的目的是节省空间,所以整个位段的字节大小应该就是2+5+10+30=47个比特位。

    按照一个字节等于八个比特位的运算原理,我们可以得到差不多6个字节。

    和同样类型的成员,但并不是位段的结构体进行对比:

    1. struct A
    2. {
    3. int _a;
    4. int _b;
    5. int _c;
    6. int _d;
    7. };

    根据结构体的对齐规则,以上结构体需要在内存中占据16个字节,和位段相比起来,位段确实是节约了空间。

    但是,位段真的如我们所算计的一样,是所有比特位相加而后在进行比特位和字节的换算吗?

    答案并不是。

    我们先后求以上两个代码,得到以下结果:

     没错,位段其实实际上所占据的字节是八个!

    这里涉及了位段的内存分配问题。

    位段的内存分配:

    分配规则:

    1. 位段的成员可以是int unsigned int signed int 或者是char 等类型
    2. 位段的空间上是按照需要以4个字节 ( int )或者1个字节(char )的方式来开的。
    3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

    从规则得知,位段是根据成员的变量来进行开辟空间的,而不是将所有的比特位放置一个空间内,是先开辟对应类型的空间后再放置内容。

    但是开辟空间后也会产生一系列的问题,这也是位段的不确定因素的原因。

    问题1:当开辟了内存后,内存中每个比特位从右向左使用?还是从左向右使用? 这个不确定。

    例如:

    1. struct S
    2. {
    3. char _a:3;
    4. char _b:4;
    5. char _c:5;
    6. char _d:4;
    7. };

    因为是char类型,占据一个字节,也就是八个比特位,所以我们率先开辟八个比特位空间。

    那么问题来了,变量a是只能放置三个比特位再这一个字节中,但我们是从空间的右边往左边放,还是从字节的左边往右边放?

    如果,从空间的右边往左边放,分别放了a(三个比特位)和b(四个比特位),那么剩下的那一个比特位(一个字节一共八个比特位),是否需要放置下一个变量的比特位?

    问题2:当前面时候,剩余的空间不足下一个成员使用的时候,剩余的空间是否使用?这个不确定。

    若我们下一个变量c(五个比特位)在开辟一个空间进行放置,后面的变量d(四个比特位)也是如此 ,(c所占据的字节中还剩下三个比特位,不够d所有的比特位)

    那么这里一共就占据了三个字节,但答案真的是如此吗?

    答案真是如此,就占据了三个字节。

    所以我们得到了位段再VS编译器中,就是如此分配内存的! ——注!仅仅是VS编译器。 

    同时我们也得到结论:位段其实也会浪费空间,但是相比于普通结构体而言,浪费的更少。

    普通结构体:一共占据了四个字节。

    1. struct S
    2. {
    3. char _a;
    4. char _b;
    5. char _c;
    6. char _d;
    7. };

    所以最后还是节约了空间。

    探究VS中的位段内存分布情况: 

    1. struct S
    2. {
    3. char a:3;
    4. char b:4;
    5. char c:5;
    6. char d:4;
    7. };
    8. struct S s = {0};
    9. s.a = 10;
    10. s.b = 12;
    11. s.c = 3;
    12. s.d = 4;

    第一步, struct S s = {0};将结构体变量初始化为0

    s.a =10 将10的二进制放到char a中,但是a只能放3个比特位,而10的二进制是1010,所以只能放三个比特位,从右到左截取三个进行存放。

    第二步、b=12,b可以放四个比特位,12的二进制是1100,所以四个放入刚刚a放入的空间。

     第三步,该空间所剩下的比特位不够变量c(5个比特位)进行存放,所以另外开辟一共空间放入c的比特位。

    而因为c是3,占据五个比特位,3的二进制是00011,所以存入五个进去。

    第四步、同样d也需要开辟空间,且d需要四个比特位。d是4 二进制位是0100,存入新开辟的空间。

    第五步、最后按照四个比特位(二进制位)一个十六进制位进行转化,最后再内存中的展示也是如此。

    总结图:

    以上都是VS编译器中出现的位段现象! 

    位段的跨平台问题:

    • int 位段被当成有符号数还是⽆符号数是不确定的。
    • 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。
    • 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
    • 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。 

    总结:

    位段在不同的编译器不同的平台效果不一样, 所以建议在一定要需要节约内存的时候使用位段,若使用位段,只能不同的平台写不同的代码。

    注意事项:

    • 位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。
    • 内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。
    • 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。 
    1. struct A
    2. {
    3. int _a : 2;
    4. int _b : 5;
    5. int _c : 10;
    6. int _d : 30;
    7. };
    8. int main()
    9. {
    10. struct A sa = {0};
    11. scanf("%d", &sa._b);//这是错误的
    12. //正确的⽰范
    13. int b = 0;
    14. scanf("%d", &b);
    15. sa._b = b;
    16. return 0;
    17. }

    热知识:

    1. bit就是位,也叫比特位,是计算机表示数据最小的单位
    2. byte就是字节
    3. 1byte=8bit
    4. 1byte就是1B
    5. 一个字符=2字节
    6. 1KB=1024B

       1.字节就是Byte,也是B

       2.位就是bit也是b

       3.转换关系如下:

       1)1KB=1024B

       2) 1B= 8b

     

    问题返回:

    让我们返回最开始的例子:

    1. 位段:
    2. struct A
    3. {
    4. int _a:2;
    5. int _b:5;
    6. int _c:10;
    7. int _d:30;
    8. };
    9. 结构体:
    10. struct A
    11. {
    12. int _a;
    13. int _b;
    14. int _c;
    15. int _d;
    16. };
    1. 按照以上的规则 2+5+10+30=47个字节,但是按照字节内部的比特位分布情况
    2. a和b占据一个字节,c单独占据两个字节,而d占据五个字节,一共七个字节,但是按照字节的开辟空间的原理,是按照先开辟空间,后放置比特位,也就是说,最后的字节数其实要看开辟空间的类型的。
    3. 在这里是int类型,也就是说是4个字节4个字节开辟空间的,所以应该是八个字节。 

    经典例题:

    1. int main()
    2. {
    3. unsigned char puc[4]; //无符号char类型数组
    4. struct tagPIM
    5. {
    6. unsigned char ucPim1;
    7. unsigned char ucData0 : 1;
    8. unsigned char ucData1 : 2;
    9. unsigned char ucData2 : 3;
    10. }*pstPimData; //一个使用了位段的结构体变量
    11. pstPimData = (struct tagPIM*)puc; //数组被强转为了结构体tagPIM * 类型
    12. memset(puc,0,4);//使用了memset 将puc中的四个字节进行改变,把四个字节变成了0
    13. //因为puc是char类型,也就是把puc内部元素初始化为0
    14. pstPimData->ucPim1 = 2;
    15. pstPimData->ucData0 = 3;
    16. pstPimData->ucData1 = 4;
    17. pstPimData->ucData2 = 5;
    18. //以上是赋值阶段
    19. printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
    20. return 0;
    21. }

    解析:

    •  根据以上所学的位段知识,我们得到,在结构的位段中,    unsigned char ucPim1;占据一个字节,unsigned char ucData0 : 1;unsigned char ucData1 : 2;以及unsigned char ucData2 : 3;一共占据一个字节,所以结构体位段,占据了两个字节!
    • pstPimData = (struct tagPIM*)puc;  在强转后,将puc的地址交予了指针变量pstPimData

      pstPimData->ucPim1 = 2; 
      pstPimData->ucData0 = 3;
      pstPimData->ucData1 = 4;
      pstPimData->ucData2 = 5;

    以上是一个结构体成员赋值阶段,但是,在赋值的同时我们也要注意 位段!我们需要按照位段的指示放置元素!——注意本题使用的位段在VS编译器中实现!

    • pstPimData->ucPim1 = 2; 放置2的二进制数位,因为该成员没有位段结构,且占据了一个字节,所以直接放入即可,0000 0010 放入2的二进制数位的八个比特位。
    • pstPimData->ucData0 = 3;因为该成员的位段是1,所以在将3变为二进制数位后,从右到左数一个比特位,按照从右到左的方式放入第二个字节中,占据第二个字节中的一个比特位。
    • pstPimData->ucData1 = 4;步骤如上一步一样,最后在前一步完成的基础上,从右到左的在第二个字节中,放入两个比特位
    • pstPimData->ucData2 = 5;也是如此。

    图解:

    注意:

    因为 pstPimData = (struct tagPIM*)puc; 的原因,所以结构体成员放入的空间是puc数组的,且打印的时候是使用%02x,也就是十六进制位数,且强制两位,所以按照每四个比特位换一个十六进制位的功能,最后答案是:02 29 00 00

    关于memset的使用:memcmp 与 memset_明 日 香的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/2301_76445610/article/details/132617490?spm=1001.2014.3001.5501  


  • 相关阅读:
    济南槐荫吴家堡 国稻种芯·中国水稻节:山东稻出黄河大米
    在原生APP中集成Unity容器
    AMD 显卡Radeon Super Resolution(Radeon显卡超分辨率) 功能,你开启了么?
    喜报 | 再度中标南网项目!AR 开启电力远程运维新智慧
    K8S安装过程五:制作与生成证书
    javascript关于NaN ,isNaN,Number.isNaN必须知道的知识
    C++STL详解(三)list的使用及其模拟实现
    使用pytest和allure框架实现自动化测试报告优化
    OceanBase荣获OSCAR两项大奖,开源已成主流开发模式
    如何在.net6webapi中记录每次接口请求的日志
  • 原文地址:https://blog.csdn.net/2301_76445610/article/details/132949978