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


    一.结构体

    1 结构体的声明

    1.1 结构的基础知识

    结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

    1.2 结构的声明

    struct tag//结构体标签
    {
    	member-list;//成员列表
    }variable-list;//变量列表
    
    • 1
    • 2
    • 3
    • 4

    注意:结构的声明不用初始化,它只是一个类型。比如int 是整形,float 是单精度浮点型。

    例如描述一个学生:

    struct Stu
    {
        //学生的相关属性
        char name[20];//名字
        int age;//年龄
        char sex[5];//性别
        char id[20];//学号
    }; //分号不能丢
    struct Stu
    {
        char name[20];
        int age;
        char sex[5];
        char id[20];
    }s1,s2;//s1,s2是结构体类型的变量——全局变量
    
    int main()
    {
        struct Stu s3;//s3是局部变量
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1.3 特殊的声明(匿名结构体类型)

    在声明结构的时候,可以不完全的声明。

    比如:

    struct 
    {
        char name[30];
        int age;
    }s1;
    
    int main()
    {
        struct s3;//错误
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意:匿名结构体类型只能使用一次。

    //匿名结构体类型
    struct
    {
        int a;
        char b;
        float c;
    }x;
    struct
    {
        int a;
        char b;
        float c;
    }a[20], *p;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面的两个结构在声明的时候省略掉了结构体标签(tag)。

    那么问题来了?在上面代码的基础上,下面的代码合法吗?

    p = &x;
    
    • 1

    警告:从“ * ” 到“ * ”的类型不兼容

    编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。

    1.4 typedef 重命名结构体指针类型

    //方法1
    typedef struct Node
    {
    	int data;
    	struct Node* next;
    }* linklist;
    
    //方法2
    struct Node
    {
    	int data;
    	struct Node* next;
    };
    typedef struct Node* linklist;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2 结构的自引用

    //代码1
    struct Node
    {
        int data;
        struct Node next;
    };
    //可行否?
    如果可以,那sizeof(struct Node)是多少?
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在结构体中包含一个类型为该结构本身的成员这种方法是不可行的,相当于无限套娃,程序会崩溃。

    image-20220727230506764

    正确的自引用方式:

    //代码2
    struct Node
    {
        int data;
        struct Node* next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结构体类型中包含一个同类型的结构体指针。

    image-20220727231123575

    //代码3
    typedef struct
    {
        int data;
        Node* next;
    }Node;
    //这样写可行吗?
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    不可行。typedef的使用必须存在匿名结构体类型,然后再对其进行重命名;而类型Node又是对存在的类型struct{int data; Node* next;}重命名产生的,类似于先有鸡还是先有蛋的问题。上面的代码再进行类型声明时Node* next中Node类型存不存在,你并不知道。造成整个结构体类型的声明是不正确的,也就无法进行类型重命名。

    //解决方案
    typedef struct Node
    {
        int data;
        struct Node* next;
    }Node;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    有了结构体类型,那如何定义变量,其实很简单。

    struct Point
    {
        int x;
        int y;
    }p1; //声明类型的同时定义变量p1
    struct Point p2; //定义结构体变量p2
    //初始化:定义变量的同时赋初值。
    struct Point p3 = {1, 2};
    
    struct score
    {
        int n;
        char ch;
    };
    struct Stu //类型声明
    {
    	char name[15];//名字
    	int age; //年龄
        struct score s;//分数
    };
    struct Stu s1 = {"zhangsan", 20, {100, 'A'}};//初始化
    
    struct Node
    {
        int data;
        struct Point p;
        struct Node* next;
    }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
    struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    4 结构体内存对齐

    我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题:计算结构体的大小。要想清楚结构体类型的的大小,就需要掌握结构体的内存对齐这一知识点。

    这也是一个特别热门的考点: 结构体内存对齐

    4.1 宏offsetof

    这里首先要清楚偏移量的概念:

    结构体成员的偏移量是相对于结构体起始位置(首地址)的偏移量!

    利用宏offsetof可以求出偏移量

    offsetof(type,member)
    
    • 1

    参数:

    type——结构或者联合类型

    member——类型中的成员

    返回值:

    返回成员相对于类型首地址的偏移量,一个size_t类型的值。

    使用实例:

    #include
    #include
    struct S1
    {
        char c1;
        int i;
        char c2;
    };
    int main()
    {
        printf("%d\n",offsetof(struct s1, c1));
        printf("%d\n",offsetof(struct s1, i));
        printf("%d\n",offsetof(struct s1, c2));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:image-20220728002346314

    4.2 结构体内存对齐规则

    考点 如何计算? 首先得掌握结构体的对齐规则:

    1. 第一个成员在与结构体变量偏移量为0的地址处。

    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

    对齐数 = 编译器默认的一个对齐数与该成员大小的较小值

    VS中默认的对齐数的值为8,linux系统没有默认对齐数

    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    我们结合题目理解结构体的内存对齐

    练习1:

    struct S1
    {
    	char c1;
        int i;
        char c2;
    };
    struct S2
    {
        char c1;
        char c2;
        int i;
    };
    int main()
    {
    	struct S1 s1;
        struct S2 s2;
        printf("%d\n", sizeof(s1));
        printf("%d\n", sizeof(s2));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行结果:image-20220728155802612

    具体分析:image-20220728155258424

    练习2:

    #include
    struct S3
    {
    	double d;
    	char c;
    	int i;
    };
    
    int main()
    {
    	printf("%d\n", sizeof(struct S3));
        //struct S3 s3;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:image-20220728160518456

    具体分析:image-20220728162440693

    练习3:结构体嵌套问题

    #include
    struct S3
    {
    	double d;
    	char c;
    	int i;
    };
    
    struct S4
    {
    	char c1;
    	struct S3 s3;
    	double d;
    };
    int main()
    {
    	printf("%d\n", sizeof(struct S4));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    运行结果:image-20220728163637599

    具体分析:image-20220728171922774

    为什么存在内存对齐?

    大部分的参考资料都是如是说的:

    1. 平台原因(移植原因)

    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    2. 性能原因

    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

    image-20220728182930704

    总体来说:结构体的内存对齐是拿空间来换取时间的做法。

    那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

    让占用空间小的成员尽量集中在一起。

    //例如:
    struct S1
    {
        char c1;
        int i;
        char c2;
    };
    struct S2
    {
        char c1;
        char c2;
        int i;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

    4.3 修改默认对齐数

    之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

    #include 
    //#pragmea once 头文件中使用,功能是:防止头文件被多次引用
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    结果:image-20220728200447606

    **结论:**结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。

    5 结构体传参

    直接上代码:

    struct S
    {
        int data[1000];
        int num;
    };
    struct S s = {{1,2,3,4}, 1000};
    //结构体传参
    void print1(struct S s)
    {
        int i = 0;
        for(i = 0; i < 4; i++)
        {
            printf("%d ",s.data[i]);
        }
        printf("%d\n", s.num);
    }
    //结构体地址传参
    void print2(const struct S* ps)
    {
        int i = 0;
        for(i = 0; i < 4; i++)
        {
            printf("%d ",ps->data[i]);
        }
    	printf("%d\n", ps->num);
    }
    int main()
    {
    	print1(s); //传结构体
    	print2(&s); //传地址
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    上面的 print1和 print2函数哪个好些?

    答案是:首选print2函数。 原因:

    函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

    如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

    如果传递一个地址,地址无非就是4/8个字节,空间较小,效率较高。

    传结构体无法改变s的数据,但是传地址,ps可以修改s的数据,这种方式不够安全,我们可以用const修饰。

    结论: 结构体传参的时候,要传结构体的地址。

    6 位段

    结构体讲完就得讲讲结构体实现 位段 的能力。

    6.1 什么是位段

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

    1.位段的成员必须是 int、unsigned int 或signed int。

    2.位段的成员名后边有一个冒号和一个数字。

    比如:

    struct A
    {
    int _a:2;//分配2个bit位
    int _b:5;//分配5个bit位
    int _c:10;//分配10个bit位
    int _d:30;//分配30个bit位
    };
    //A就是一个位段类型。
    //那位段A的大小是多少?
    int mian()
    {
        printf("%d\n",sizeof(struct A));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    结果:image-20220728203600437

    位段A的类型是int类型,在开辟内存空间时是以4个字节的大小来开辟的,先给A类型开辟4个字节的空间,_ a、_ b、_c 一共占据了17个bit位,我们把他们分配到刚刚开辟的4个字节的空间内。还剩下15个字节的空间,而 _ d占据30个bit位,不能存下。我们再开辟4个字节的空间,所以位段A的大小为8字节。

    注意:_d有没有使用给A第一次分配的4字节剩下的15个bit位,这个是不能确定的!

    6.2 位段的内存分配

    1. 位段的成员可以是 int ,unsigned int, signed int或者是 char(属于整形家族)类型

    2. 位段的空间上是按照需要以4个字节( int)或者1个字节( char)的方式来开辟的

    3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

    //一个例子
    #include
    struct S
    {
    	char a : 3;
    	char b : 4;
    	char c : 5;
    	char d : 4;
    };
    
    int main()
    {
    	struct S s = { 0 };
    	printf("%d\n", 
        sizeof(struct S));
    
    	s.a = 10;
    	s.b = 12;
    	s.c = 3;
    	s.d = 4;
    	return 0;
    }
    //空间是如何开辟的?
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    通过调试,我们可以发现在VS2022(X86)的环境下,位段的内存分配如下:

    image-20220728211036534

    6.3 位段的跨平台问题

    1. int 位段被当成有符号数还是无符号数是不确定的

    2. 位段中最大位的数目不能确定(16位机器(int 占据2个字节)最大16,32位机器(int 占据4个字节)最大32,写成27,在16位机器会出问题)

    3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义

    4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

    总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

    6.4 位段的应用

    image-20220728212303330

    这部分内容涉及网络协议,网络编程的知识,这里不讲解。

    二.枚举

    枚举顾名思义就是一一列举。

    把可能的取值一一列举。

    比如我们现实生活中:

    • 一周的星期一到星期日是有限的7天,可以一一列举。

    • 性别有:男、女、保密,也可以一一列举。

    • 月份有12个月,也可以一一列举

    这里就可以使用枚举了。

    1 枚举类型的定义

    #include
    enum Day//星期
    {
        //枚举常量
    	Mon,//0
    	Tues,//1
    	Wed,//2
    	Thur,//3
    	Fri,//4
    	Sat,//5
    	Sun//6
    };
    enum Sex//性别
    {
    	MALE,//0
    	FEMALE,//1
    	SECRET//2
    };
    enum Color//颜色
    {
    	RED,//0
    	GREEN,//1
    	BLUE//2
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    以上定义的 enum Day, enum Sex, enum Color都是枚举类型。

    {}中的内容是枚举类型的可能取值,也叫 枚举常量 。

    这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:

    enum Color//颜色
    {
        RED=1,
        GREEN=2,
        BLUE=4
    };
    enum Day//星期
    {
    	//枚举常量
    	Mon=1,
    	Tues,//2
    	Wed,//3
    	Thur,//4
    	Fri,//5
    	Sat,//6
    	Sun//7
    };
    
    int main()
    {
        enum Day d = Fri;//枚举类型赋值
    	printf("%d\n", Mon);
    	printf("%d\n", Tues);//2
    	printf("%d\n", Wed);//3
    	printf("%d\n", Thur);//4
    	printf("%d\n", Fri);//5
    	printf("%d\n", Sat);//6
    	printf("%d\n", Sun);//7
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2 枚举的优点

    为什么使用枚举?

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

    枚举的优点:

    1. 增加代码的可读性和可维护性

    2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

    3. 防止了命名污染(封装)

    4. 便于调试

    5. 使用方便,一次可以定义多个常量

    3 枚举的使用

    enum Color//颜色
    {
    RED=1,
    GREEN=2,
    BLUE=4
    };
    enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
    clr = 5; //这样赋值是错误的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    三.联合(共用体)

    1 联合类型的定义

    联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。比如:

    #include
    union Un
    {
    	int a;
    	char c;
    };
    
    struct St
    {
    	int a;
    	char c;
    };
    
    int main()
    {
    	union Un u;
    	printf("%d\n", sizeof(u));//?
    	printf("%p\n", &u);
    	printf("%p\n", &u.a);
    	printf("%p\n", &u.c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行结果:image-20220728220155639

    分析:image-20220728222153778

    2 联合的特点

    1. 联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

    2. 联合的成员在使用时不会同时使用所有成员,一个才操作只会使用一个成员,避免成员之间相互影响。

    union Un
    {
        int i;
        char c;
    };
    union Un un;
    // 下面输出的结果是一样的吗?
    int main()
    {
        //结果一样
        printf("%d\n", &(un.i));
    	printf("%d\n", &(un.c));
        //下面输出的结果是什么?
        un.i = 0x11223344;
        un.c = 0x55;
        //改变c的同时,i也会改变
        printf("%x\n", un.i)
        return 0;   
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20220728222804563

    面试题:

    • 判断当前计算机的大小端存储
    #include
    int check_sys1()
    {
        int a = 1;
        return *(char*)&a;
    }
    int check_sys2()
    {
        union
        {
            char c;
            int i;
        }u;
        u.i = 1;
        //返回1是小端,返回0是大端
        return u.c;
    }
    int main()
    {
        //int a = 1;//0x 00 00 00 01
        //低————>高
        //01 00 00 00 --小端
        //00 00 00 01 --大端
        int ret = check_sys2();
        if(ret)
        	printf("小端\n");
        else
            printf("大端\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    image-20220728230312348

    3 联合大小的计算

    1. 联合的大小至少是最大成员的大小

    2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

    比如:

    #include
    union Un1
    {
    	char c[5];
        //char c1;
        //char c2;
        //char c3;
        //char c4;
        //char c5;
    	int i;
    };
    union Un2
    {
    	short c[7];
    	int i;
    };
    int main()
    {
    	printf("%d\n", sizeof(union Un1));
    	printf("%d\n", sizeof(union Un2));	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行结果:image-20220728231458209

    分析:

    Un1中 char c[5] 的大小为5,对齐数为1;i 的大小为4,对齐数为4;c[5]和i共用同一块空间,5不是4的倍数,所以联合的大小应该为4的倍数,且大于等于i的大小,最小为8;

    Un2中 short c[7] 的大小为14,对齐数为2;i 的大小为4,对齐数为4;c[7]和i共用同一块空间,14不是4的倍数,所以联合的大小应该为4的倍数,最小为16。

  • 相关阅读:
    TiKV 源码阅读三部曲(三)写流程
    PPT 架构师三板斧
    关于 SAP UI5 应用 ui5.yaml 里的 paths 映射问题
    【Flyweight模式】C++设计模式——享元模式
    cookie和session区别
    鸿蒙应用开发之数据管理
    pygame入门之环境配置
    【MATLAB】BiGRU神经网络回归预测算法
    【PAT甲级 - C++题解】1065 A+B and C (64bit)
    DailyPractice.2023.10.14
  • 原文地址:https://blog.csdn.net/m0_64224788/article/details/126160583