• 自定义类型:结构体,枚举,联合的学习



    在这里插入图片描述

    一、结构体

    1.1 结构的基础知识

    结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。说人话就是一个里面可以有不同的类型变量成员组合在一起的集合体;

    1.2 结构的声明

    例如描述一个学生:
    struct Stu
    {
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
    }; //分号不能丢

    1.3 特殊的声明

    在声明结构的时候,可以不完全的声明。
    比如:
    //匿名结构体类型
    struct
    {
    int a;
    char b;
    float c;
    }x;
    struct
    {
    int a;
    char b;
    float c;
    }a[20], *p;
    //就是没有类型名字嘛,注意这样的结构体只能用一次,用完系统自动回收;
    //在上面代码的基础上,下面的代码合法吗?
    p = &x; ?
    编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的,毕竟两个都没有名字,而且只能用一次,这样规定很合理。

    1.4 结构的自引用

    在结构中包含一个类型为该结构本身的成员是否可以呢?就长下面这样子:
    struct Node
    {
    int data;
    struct Node next;
    };
    //可行否?
    如果可以,那sizeof(struct Node)是多少?
    这样应该很好理解吧,大概率是死循环的,因为一看就是无限套娃的模式,要实现上面的要求,应该参考一下数据结构里面的一种叫链表的数据结构;
    正确代码如下:
    struct Node
    {
    int data;
    struct Node* next;
    };//用结构体指针存储下一个结构体的地址,通过指针就可以找到下一个结构体了;一个链着下一个,也就可以形象地看出是一条链子嘛;
    typedef struct
    {
    int data;
    Node* next;
    }Node;
    //这样写代码,可行否?
    答案是不行的,因为typedef要先存在结构体类型才能重命名,但是这个代码的结构体指针将已经重命名完后的结构体名字进行定义结构体指针,是错误的,这个名字应该是将结构体里面的数据都定义完成后才重命名的,也就是说结构体指针的定义在重命名前,所以定义结构体指针的时候不可以使用重命名后的结构体类型名;

    1.5 结构体变量的定义和初始化

    struct Point
    {
    int x;
    int y;
    }p1; //声明类型的同时定义变量p1
    struct Point p2; //定义结构体变量p2,这个定义就是先声明类型后定义变量
    struct Point p3 = {x, y};//初始化:定义变量的同时赋初值。
    struct Stu //类型声明
    {
    char name[15];//名字
    int age; //年龄
    };
    struct Stu s = {“zhangsan”, 20};//初始化
    struct Node
    {
    int data;
    struct Point p;
    struct Node* next;
    }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
    struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

    1.6 结构体内存对齐(很重要)

    现在我们深入讨论一个问题:计算结构体的大小。
    这也是一个特别热门的考点: 结构体内存对齐
    struct S1{
    char c1;
    int i;
    char c2;
    };printf("%d\n", sizeof(struct S1));
    知道这个结构体占多大空间吗?这就要涉及结构体内存对齐的知识点了,结构体所占空间大小不是简单的变量空间大小相加;
    首先得掌握结构体的对齐规则:

    1. 第一个成员在与结构体变量偏移量为0的地址处。
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
      对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
      VS中默认的值为8,其它编译器都没有默认对齐数,有对齐数就是自身;
    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    在这里插入图片描述

    struct S2
    {
    char c1;
    char c2;
    int i;
    };
    printf("%d\n", sizeof(struct S2));

    在这里插入图片描述

    struct S3
    {
    double d;
    char c;
    int i;
    };
    printf("%d\n", sizeof(struct S3));
    其实有了上面两题的经验,不用话图直接都可以想出答案,首先就是最大对齐数的问题,在没有使用
    #pragma pack(要修改的默认对齐数大小)的情况下,只有vs编译器有默认的8对齐数,其它编译器例如
    Linux上没有默认对齐数,默认对齐数就是自身大小;反正影响不大,只要没有该最大对齐数,最大也就是8,正常变量类型最大都是double,long long这样的8个字节的,和最大对齐数也是一样的,所以说算的时候跟没有8这个默认最大对齐数是一样的,只是我们要懂就是了;
    double对齐数8,没有大于默认对齐数,占8个字节了,然后是char类型,对齐数是1,任意偏移量都可以,所以连着放在偏移量为8的位置,这个时候就有9个字节了,偏移量从0开始的嘛;然后就是int类型,4个字节对齐数,要4的倍数,8的位置没了,只能在12开始了,所以中间的9~11这三个字节就算是浪费了的,在开4个字节给变量i,总共就是16个字节了,然后变量最大的对齐数是8,然后16个字节也是8的倍数,可以,所以最终答案是16;

    下一题解释的时候就没有那么多细节了,上面一题已经将细节都将得很清楚了;

    struct S3
    {
    double d;
    char c;
    int i;
    };
    printf("%d\n", sizeof(struct S3));
    //练习4-结构体嵌套问题
    struct S4
    {
    char c1;
    struct S3 s3;
    double d;
    };
    printf("%d\n", sizeof(struct S4));
    首先还是将上面知识点回顾一下,是关于结构体嵌套的;
    如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    这样说吧,嵌套的结构体还是正常算,c1对齐数1,一个字节,结构体S3中,d从偏移量8开始开8个字节给d,那么偏移量1~7浪费了,c对齐数是1,连着放到偏移量16了,i对齐数是·4,只能从20开始了,开4个字节,最后是S4中的d从24开始,这个d对齐数是8,这个时候偏移量就到23了,也就是开了32个字节,然后最大对齐数是8,32是8的倍数,答案就是32了;

    1.7 为什么存在内存对齐?

    1. 平台原因(移植原因):
      不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
      定类型的数据,否则抛出硬件异常。
    2. 性能原因:
      数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
      原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
      问。
      总体来说:结构体的内存对齐是拿空间来换取时间的做法。
      那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
      让占用空间小的成员尽量集中在一起。
      //例如:
      struct S1
      {
      char c1;
      int i;
      char c2;
      };
      struct S2
      {
      char c1;
      char c2;
      int i;
      };
      S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

    1.8 修改默认对齐数

    #include
    #pragma pack(8)//设置默认对齐数为8
    struct S1
    {
    char c1;
    int i;
    char c2;
    };
    #pragma pack()//取消设置的默认对齐数,还原为默认
    #pragma pack(1)//设置默认对齐数为1
    struct S2
    {
    char c1;
    int i;
    char c2;
    };
    #pragma pack()//取消设置的默认对齐数,还原为默认
    int main()
    {
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
    }
    第一个结构体默认最大对齐数是8,第二个是1,所以第一个的话还是一样做,第二个的话所有的对齐数都变成了1,所有这个时候每个字节都是满的,没有空间浪费;所有第二个答案是6;
    知道如何得到结构体每个变量的偏移量吗?
    先吧该结构体的地址设置成0,如何再得到变量的地址,不就是偏移量了吗
    所以偏移量n=(size_t)&(((struct S1*)0)->a),n就是偏移量大小了;

    1.9 结构体传参

    结构体传参的时候,要传结构体的地址。
    原因:
    函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
    如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
    下降。要改变结构体内容的时候传地址才行;

    二、位段

    2.1 什么是位段?

    位段的声明和结构是类似的,有两个不同:
    1.位段的成员必须是 int、unsigned int 或signed int (char也行)。
    2.位段的成员名后边有一个冒号和一个数字。
    struct A
    {
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
    };

    2.2 位段的内存分配

    1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
    2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
    3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
      //一个例子
      struct S
      {
      char a:3;
      char b:4;
      char c:5;
      char d:4;
      };
      struct S s = {0};
      s.a = 10;
      s.b = 12;
      s.c = 3;
      s.d = 4;
      //空间是如何开辟的?

    在这里插入图片描述
    在vs上的结果是先开辟1个字节,因为是char类型嘛,要是int类型就先开辟4个字节了,然后截断位段变量后面的数字,变量a是3个比特位,把10截断二进制的三个位放进去,然后是b,4个比特位,可以放得下第一个字节上,然后c是5个比特位,vs上是没有把第一个字节的最后一位比特位利用上,直接再开一个字节放5个比特位放c,然后一样的,d要4个比特位,第二个比特位不够,所以再开一个字节,第二个字节的剩下比特位没有利用,d是直接放4个比特位的;字节之间是有大小端的,一个字节内部是没有的;

    2.3 位段的跨平台问题

    1. int 位段被当成有符号数还是无符号数是不确定的。
    2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
      器会出问题。
    3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
    4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
      舍弃剩余的位还是利用,这是不确定的。
      总结:
      跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

    2.4 位段的应用

    在这里插入图片描述
    很多我们生活中的数据为了省内存和方便或者效率等等原因,会采用位段存储数据;

    枚举

    枚举顾名思义就是一 一列举。
    把可能的取值一一列举。
    比如我们现实生活中:
    一周的星期一到星期日是有限的7天,可以一一列举。
    性别有:男、女、保密,也可以一一列举。
    月份有12个月,也可以一一列举
    这里就可以使用枚举了。

    3.1 枚举类型的定义

    enum Day//星期
    {
    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
    };
    enum Sex//性别
    {
    MALE,
    FEMALE,
    SECRET
    };
    enum Color//颜色
    {
    RED,
    GREEN,
    BLUE
    };
    以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
    {}中的内容是枚举类型的可能取值,也叫 枚举常量 。
    这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
    例如:
    enum Color//颜色
    {
    RED=1,
    GREEN=2,
    BLUE=4
    };

    3.2 枚举的优点

    我们可以使用 #define 定义常量,为什么非要使用枚举?
    枚举的优点:

    1. 增加代码的可读性和可维护性
    2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
    3. 防止了命名污染(封装)
    4. 便于调试
    5. 使用方便,一次可以定义多个常量

    3.3 枚举的使用

    enum Color//颜色
    {
    RED=1,
    GREEN=2,
    BLUE=4
    };
    enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
    clr = 5; //ok?? 肯定不行
    枚举类型的赋值只能用枚举内的枚举常量;

    4. 联合(共用体)

    4.1 联合类型的定义

    联合也是一种特殊的自定义类型
    这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
    比如:
    //联合类型的声明
    union Un
    {
    char c;
    int i;
    };
    //联合变量的定义
    union Un un;
    //计算连个变量的大小
    printf("%d\n", sizeof(un));

    4.2 联合的特点

    联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
    合至少得有能力保存最大的那个成员)。
    union Un
    {
    int i;
    char c;
    };
    union Un un;
    // 下面输出的结果是一样的吗?答案是一样的,总共就开了4个字节,c和i共用一个字节,其它三个归 i 变量;
    printf("%d\n", &(un.i));
    printf("%d\n", &(un.c));
    //下面输出的结果是什么?
    un.i = 0x11223344;
    un.c = 0x55;
    printf("%x\n", un.i);
    //变量 i 先赋值,然后再是c,所以c变量把 i 的一个字节内容覆盖了,所以是11223355;

    4.3 联合大小的计算

    联合的大小至少是最大成员的大小。
    当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
    比如:
    union Un1
    {
    char c[5];
    int i;
    };
    union Un2
    {
    short c[7];
    int i;
    };
    //下面输出的结果是什么?
    printf("%d\n", sizeof(union Un1));
    printf("%d\n", sizeof(union Un2));
    //char对齐数是1,开了5个字节,然后变量 i 是4个字节,共用4个字节,但是最大对齐数是4,现在开了5个字节,不是4的倍数,所以要浪费3个字节,开8个字节才能到4的倍数;
    第二个联合体的话short c是开了14个字节,然后变量 i 还是共用,最大对齐数是4,14不是4的倍数,所以浪费2个字节到16个字节,才是4的倍数;

    总结

    在这里插入图片描述

  • 相关阅读:
    React之diff原理
    达梦数据库答案
    CSS学习————文本属性(2)
    Redis事务+秒杀案例
    入门人工智能 ——自然语言处理介绍,并使用 Python 进行文本情感分析(5)
    【Python 千题 —— 基础篇】奇数列表
    双指针——移动零
    Java异常编程题(巩固语法)
    ES中 Nested 类型的原理和使用
    python绘制立体玫瑰花
  • 原文地址:https://blog.csdn.net/qq_68844357/article/details/125746979