• 『C语言进阶』自定义类型详解


    在这里插入图片描述
    🔥博客主页 小羊失眠啦.
    🔖系列专栏 C语言LinuxCpolar
    ❤️感谢大家点赞👍收藏⭐评论✍️


    在这里插入图片描述

    前言

    C语言提供了一些由系统已定义好的数据类型,如:int,float,char等,用户可以在程序中用它们定义变量,解决一般的问题,但我们要处理的问题往往比较复杂,只有系统提供的类型还不能满足应用的需求,C语言允许用户根据自己建立一些数据类型,并用它来定义变量,比如结构体,枚举,联合体这些自定义类型

    接下来小羊就带铁汁们学习关于结构体,枚举以及联合体的相关内容~~


    一、结构体

    1.1 结构体的基础知识

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

    声明结构体类型的一般形式:

    struct 结构体名

    ​ {

    ​ //成员变量,是用来描述结构体对象的相关属性

    ​ 成员列表

    ​ };

    1.2 结构体的声明

    1.先声明结构体类型,再定义该类型的变量

    struct stu
    {			
    	char name[20];		
    	int stuid[20];		
    	int age;
        int height;
    };
     
    int main()
    {
        struct stu s1;
        struct stu s2;
        return 0;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.在声明类型的同时定义:

    struct stu
    {			
    	char name[20];		
    	int stuid[20];		
    	int age;
        int height;
    }s1,s2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这种定义结构体的一般形式为:

    struct 结构体名

    {

    ​ 成员列表;

    }变量名列表;

    1.3 结构体的特殊声明

    匿名结构体类型

    #include
    struct
    {
    	char a;
    	int b;
    	double c;
    }d;//结构体必须在这里命名,而且只能使用一次
    
    struct
    {
    	char a;
    	int b;
    	double c;
    }* ps;
    
    int main()
    {
    	ps = &d;//这里不能这样使用,这是非法的
    	//编译器认为等号两边是不同的结构体类型
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    仔细观察上面的这两个结构是不是除了变量名不一样,其他都完全一样

    那问题来了,这两个结构体是相同的结构体吗???

    答案当然不是,省略掉结构体标签后,编译器会认为这两个结构体是不一样的,即使这两个结构体成员是一样的,编译器也会把上面的两个结构体声明当成完全不同的类型

    1.4 结构体的自引用

    struct Node
    {
         int Data;           
         struct Node* Next; 
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #include
    
    typedef struct Node//重命名这个结构体为Node
    {
        int Data;            
        struct Node* Next; 
    }Node;
    
    int main()
    {
        struct Node n1 = { 0 };//写法一
        Node n2 = { 0 };//写法二
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.5 结构体的初始化和访问

    初始化:

    struct Student
    {
    	int num;
    	char name[20];
    	char sex[5];
    	int age;
    }s1 = { 1,"李四","女",19 };
    
    
    int main()
    {
    	struct Student s2 = { 2,"张三","男",18 };
    	printf("%d %s %s %d\n", s1.num, s1.name, s1.sex, s1.age);
    	printf("%d %s %s %d\n", s2.num, s2.name, s2.sex, s2.age);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    结构体嵌套初始化:

    struct Point
    {
        int a;
        double b;
    };
    
    struct Node
    {
    	int data;
    	struct Point p;
    	struct Node* next;
    }n1 = { 10, {4,5}, NULL };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结构体访问:

    • p.成员名
    • (*p).成员名
    • p->成员名

    1.6 结构体内存对齐

    结算结构体的大小,是一个很热门的考点:结构体内存对齐

    #include
    
    struct S1
    {
    	char s1;
    	int i;
    	char s2;
    };
    
    struct S2
    {
    	int i;
    	char s1;
    	char s2;
    };
    
    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

    运行结果:

    12
    8
    
    • 1
    • 2

    按照我们现在的所认知,char是一个字节,int是两个字节,所以这两个结构体大小不应该是1+1+4=6个字节吗??但运行起来的结果却跟我们想的不一样,那么就让铁汁们带着疑问往下看~~

    我们可以通过宏(offsetof)来观察结构体中各成员变量距起始位置的偏移量

    offsetof:计算结构体成员变量相较于结构体起始位置的偏移量

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

    运行结果:

    12
    0
    4
    8
    
    • 1
    • 2
    • 3
    • 4

    好,接下来小羊来解开小伙伴们的疑惑~

    首先得掌握结构体的对齐规则:

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

    在这里插入图片描述

    第一个成员是s1,所以在偏移量为0处,它的对齐数是1,占一个字节;

    第二个成员是i,它的对齐数是4,所以从偏移量为4开始,占四个字节;

    第一个成员是s2,它的对齐数是1,所以从偏移量为8开始,占一个字节;

    为什么存在内存对齐?

    1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

    总体来说:

    结构体的内存对齐是拿空间来换取时间的做法,我们让占用空间小的成员尽量集中在一起,就可以节省空间。

    1.7 修改默认对齐数

    #include 
    #pragma pack(2)//设置默认对齐数为2
    struct S1
    {
        char c1;
        int i;
        char c2;
    };
    #pragma pack()//取消设置的默认对齐数,还原为默认
    
    int main()
    {
        printf("%d\n", sizeof(struct S1));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    8
    
    • 1

    在不改变对齐数的情况下,运行结果应该是12

    结构体在对齐方式不合适的时候,我们可以自己改变默认对齐数

    1.8 结构体传参

    #include
    struct S
    {
    	int a;
    	int b;
    };
    struct S s = { 1,9 };
    
    void Print(struct S* ps)
    {
    	printf("%d\n", ps->b);
    }
    
    int main()
    {
    	Print(&s);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

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

    所以结构体传参的时候,传结构体地址


    二、位段

    2.1 什么是位段

    1. 位段的成员必须是int、unsigned int或者signed int(还可以是char类型)
    2. 位段的成员后边有一个冒号和一个数字
    #include
    struct A
    {
    	int a : 2;//a需要2个比特位
    	int b : 5;//b需要5个比特位
    	int c : 10;//c需要10个比特位
    	int d : 28;//d需要28个比特位
    };//这就是一个位段
    
    int main()
    {
    	printf("%d\n", sizeof(struct A));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:

    8
    
    • 1

    首先因为a是int类型,所以开辟了4个字节(32个比特位)的空间,a需要2个比特位,还剩30个比特位,刚好b需要5个比特位,就给b,还剩下25个比特位,刚好c需要10个比特位,给c后还剩15个比特位,而d需要28个比特位,还剩下的15个比特位不够,d就只能再开辟一个int类型空间即四个字节,所以一共是8个字节。

    这时候相信铁汁们有个这样的疑问?:

    那个还剩下来的15个比特位怎么用的?

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

    运行结果:

    3
    
    • 1

    所以在这里我们可以猜测VS2022,上一次开辟的空间剩下的不够时,是被遗弃了,并没有再次使用,直接用新开的空间。

    2.2 位段的内存分配

    • 位段的成员可以是 int 、unsigned int、signed int 或者是 char (属于整形家族)类型
    • 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
    • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
    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 = 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

    在这里插入图片描述

    2.3 位段的跨平台问题

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

    总结:

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


    三、枚举

    枚举顾名思义就是列举,把可能的取值列举。(枚举是一个常量)

    例如:

    一周是从周一到周日,是有限的,可以一一列举

    三原色,也可以一一列举

    3.1 枚举类型的定义

    #include
    enum day//星期
    {
    	Mon,
    	Tues,
    	Wed,
    	Thir,
    	Fri,
    	Sta,
    	Sun
    };
    
    enum Sex//性别
    {
    	MALE,
    	FEMALE = 3,
    	SECRET
    };
    
    enum Color//颜色
    {
    	RED,
    	GREEN = 89,
    	BLUE = 78
    };
    
    int main()
    {
    	enum Day a = Sun;
    	printf("%d\n", Mon);
    	printf("%d\n", Tues);
    	printf("%d\n", Wed);
    	printf("\n%d\n", MALE);
    	printf("%d\n", FEMALE);
    	printf("%d\n", SECRET);
    	printf("\n%d\n", RED);
    	printf("%d\n", GREEN);
    	printf("%d\n", BLUE);
    	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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    运行结果:

    0
    1
    2
    
    0
    3
    4
    
    0
    89
    78
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    以上定义的enum Day,enum Sex,enum Color都是枚举类型,{}中 的内容都是枚举类型的可能取值,也叫枚举常量

    这些可能取值都是有值的,默认从0开始,依次加1,也可以在定义时赋值初值

    3.2 的使用

    enum Color
    {
         RED=1,
         GREEN=2,
         BLUE=4
    };
     
     
    enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注意:

    • 枚举是一个常量,在定义枚举的时候(无论有没有赋值),不可以对可能取值的值进行改变,例如:Mon=3;(如果想让值发生改变,只能在定义枚举的时候进行赋值改变)

    3.3 枚举的优点

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

    四、联合(共同体)

    4.1 联合体的定义

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

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

    运行结果:

    4
    000000406BD4F784
    000000406BD4F784
    000000406BD4F784
    
    • 1
    • 2
    • 3
    • 4

    起始地址是一样的,共用一块地址,所以是4个字节

    4.2 联合体的特点

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

    习题:判断当前计算机的大小端存储:

    低位放在低地址的是小端,地位放在高地址的是大端

    常规写法:

    #include 
     
    int cheak_sys()
    {
    	int a = 1;//00 00 00 01(16进制)
    	return *((char*)&a);
    }
     
    int main()
    {
    	int ret = 0;
    	ret = cheak_sys();
    	if (ret == 1)
    		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

    用联合的方法写:

    #include 
     
    int cheak_sys()
    {
    	union Un
    	{
    		char c;
    		int i;
    	}u;
    	u.i = 1;
    	return u.c;
    }
     
    int main()
    {
    	int ret = 0;
    	ret = cheak_sys();
    	if (ret == 1)
    		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

    运行结果:

    小端
    
    • 1

    4.3 联合体大小的计算

    联合的大小至少是最大成员的大小。

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

    (和结构体一样)但是注意起始地址是一样的

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

    在这里插入图片描述

    本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,小羊一定认真修改,写出更好的文章~~

    在这里插入图片描述

  • 相关阅读:
    从 DMAIC 方法论说起,记一个长链接 bug 的排查全过程
    变量与常量
    AAC音频格式ADTS头详解
    字符串逆序 C语言
    docker常用命令整理
    民安智库(第三方市场调查公司)北京汽车神秘顾客调查
    罗技驱动默认安装位置介绍
    蚓链数字化生态系统助力企业业务数字化转型
    spark(day04)
    Apache配置ssl证书-实现https访问
  • 原文地址:https://blog.csdn.net/hsjsiwkwm/article/details/134010335