【进阶C语言】自定义类型:结构体,枚举,联合

前言

作者简介:热爱跑步的恒川,正在学习C/C++、Java、Python等。

本文收录于C语言进阶系列,本专栏主要内容为数据的存储、指针的进阶、字符串和内存函数的介绍、自定义类型结构、动态内存管理、文件操作等,持续更新!

相关专栏Python,Java等正在发展,拭目以待!


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

  • 1. 结构体
    • 1.1 结构的基础知识
    • 1.2 结构的声明
    • 1.3 特殊的声明
    • 1.4 结构的自引用
    • 1.5 结构体变量的定义和初始化
    • 1.6 结构体内存对齐
    • 1.7 修改默认对齐数
    • 1.8 结构体传参
    • 2. 位段
      • 2.1 什么是位段
      • 2.2 位段的内存分配
      • 2.3 位段的跨平台问题
      • 2.4 位段的应用
      • 3. 枚举
        • 3.1 枚举类型的定义
        • 3.2 枚举的优点
        • 3.3 枚举的使用
        • 4. 联合(共用体)
          • 4.1 联合类型的定义
          • 4.2 联合的特点
          • 4.3 联合大小的计算

            1. 结构体

            1.1 结构的基础知识

            结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。注意:之前学过的数组里的每个成员是相同类型的变量。

            1.2 结构的声明

            struct tag//tag结构体的名字
            { member-list;
            }variable-list;
            

            例如描述一个学生:

            struct Stu
            { //成员变量
            	char name[20];//名字
            	int age;//年龄
            	char sex[5];//性别
            	char id[20];//学号
            }; //分号不能丢
            
            //定义学生类型
            struct Stu
            {//成员变量
            	char name[20];
            	int age;
            	float weight;
            } s4, s5, s6;//s4、5、6都是学生//全局变量
            int main()
            {//int num = 0;
            	//我们用类型创建变量,s1是第一个学生,s2是第二个学生,s3是第三个学生
            	struct Stu s1;//局部变量
            	struct Stu s2;
            	struct Stu s3;
            	return 0;
            }
            

            1.3 特殊的声明

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

            比如:

            //匿名结构体类型
            struct 
            {char c;
            	int a;
            	double d;
            }s1;
            struct
            {char c;
            	int a;
            	double d;
            }* ps;
            int main()
            {//ps = &s1;//err
            	return 0;
            }
            

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

            那么问题来了?

            //在上面代码的基础上,下面的代码合法吗?
            ps = &s1;
            

            警告:

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

            所以是非法的。

            1.4 结构的自引用

            在结构中包含一个类型为该结构本身的成员是否可以呢?

            我想将这样一个链表连接起来,那要怎么样连接呢?

            struct Node
            {int data;
            	struct Node n;
            };
            //可行否?
            如果可以,那sizeof(struct Node)是多少?
            

            答案是当然不可行的

            正确的自引用方式:

            struct Node
            {int data;//4
            	struct Node* next;//4/8
            };
            int main()
            {struct Node n1;
            	struct Node n2;
            	n1.next =  &n2;
            	return 0;
            }
            

            当一个结构体可以找到另外一个跟自己同类型的结构体的时候就可以用这种方法,自己的类型包含一个自己的变量是绝对不行的,而应该是自己类型包含一个自己类型的指针才是可行的

            拓展一个新的知识点

            typedef struct
            {int data;
            	char c;
            } S;
            

            typedef可以对一个匿名结构体重命名定义一个新的名字,这个S不是一个变量名而是一个类型名

            在思考一个问题

            typedef struct 
            {int data;
            	Node* next;
            }Node;
            

            这样行不行?

            答案是不可以,结构体是要有一个先后顺序才行,先进行大括号里面的内容,后看大括号外面的,这里的Node还没有重命名就使用时错误的,如果你硬想这样使用,那应该这样写

            typedef struct Node
            {int data;
            	struct Node* next;
            }Node;
            

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

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

            变量的定义:

            struct S
            {int a;
            	char c;
            }s1;//全局变量
            struct S s3;//全局变量
            int main()
            {struct S s2;//局部变量
            	return 0;
            }
            

            这样写行不行?

            当然是可行的

            初始化:定义变量的同时赋初值

            struct S
            {int a;
            	char c;
            }s1;
            struct B
            {float f;
            	struct S s;
            };
            int main()
            {//int arr[10] = {1,2,3};
            	//int a = 0;
            	struct S s2 = {100, 'q'};
            	struct S s3 = {.c = 'r', .a = 2000};//用.操作可以指定我的顺序,其他都是默认顺序
            	struct B sb = { 3.14f, {200, 'w'}};//结构体里面含有结构体要再用一个大括号
            	printf("%f,%d,%c\n", sb.f, sb.s.a, sb.s.c);
            	return 0;
            }
            

            含有指针的的结构体初始化

            struct S
            {char name[100];
            	int* ptr;
            };
            int main()
            {int a = 100;
            	struct S s = {"abcdef", NULL};
            	return 0;
            }
            

            1.6 结构体内存对齐

            我们已经掌握了结构体的基本使用了。

            现在我们深入讨论一个问题:计算结构体的大小。

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

            直接上代码

            struct S1
            {int a;
            	char c;
            };
            struct S2
            {char c1;
            	int a;
            	char c2;
            };
            struct S3
            {char c1;
            	int a;
            	char c2;
            	char c3;
            }; 
            int main()
            {//探讨到底是下面三个答案的哪个
            	//5 6 7
            	//8 8 8
            	//8 12 12
            	printf("%d\n", sizeof(struct S1));
            	printf("%d\n", sizeof(struct S2));
            	printf("%d\n", sizeof(struct S3));
            	return 0;
            }
            

            答案却是 8 12 12,这是为什么呢?

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

            1. 结构体的第一个成员永远都放在0偏移处
            2. 从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍处

              这个对齐数是:成员自身大小和默认对齐数的较小值

              备注:

              VS环境下 没有默认对齐数,没有默认对齐数时,对齐数就是成员自身的大小

            3. 当成员全部存放进去后

              结构体的总大小必须是,所以成员的对齐数中最大对齐数的整数倍

              如果不够,则浪费空间对齐。

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

              当换一下位置时:

            如果不相信可以求一下他们的偏移量

            求偏移量需要用到一个offsetof宏

            #include struct S
            {char c;
            	int a;
            };
            int main()
            {struct S s = {0};
            	printf("%d\n", offsetof(struct S, c));//0
            	printf("%d\n", offsetof(struct S, a));//4
            	return 0;
            }
            

            当了解这个规则后,我们再练习一个

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

            图片讲解:

            再练习一个

            struct S3
            {double d;
            	char c;
            	int i;
            };
            

            再练习一个结构体嵌套的问题

            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. 性能原因:

              数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

              原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

            总体来说:

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

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

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

            //例如:
            struct S1
            {char c1;
            	int i;
            	char c2;
            };
            struct S2
            {char c1;
            	char c2;
            	int i;
            };
            

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

            1.7 修改默认对齐数

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

            #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()
            {//输出的结果是什么?//结果为6
            	printf("%d\n", sizeof(struct S1));
            	printf("%d\n", sizeof(struct S2));
            	return 0;
            }
            

            1.8 结构体传参

            直接上代码:

            struct S
            {int data[1000];
            	int num;
            };
            struct S s = { {1,2,3,4}, 1000 };
            //结构体传参
            void print1(struct S s)
            {printf("%d\n", s.num);
            }
            //结构体地址传参
            void print2(struct S* ps)
            {printf("%d\n", ps->num);
            }
            int main()
            {print1(s);  //传结构体
            	print2(&s); //传地址
            	return 0;
            }
            

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

            答案是:

            首选print2函数。

            原因:

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

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

            结论:

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

            2. 位段

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

            2.1 什么是位段

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

            1. 位段的成员必须是 int、unsigned int 或signed int 。
            2. 位段的成员名后边有一个冒号和一个数字。

            比如:

            struct A
            {int _a : 2;
            	int _b : 5;
            	int _c : 10;
            	int _d : 30;
            };//47个比特位
            

            A就是一个位段类型。

            那位段A的大小是多少?

            printf("%d\n", sizeof(struct A));
            

            打印结果是8

            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;
            //空间是如何开辟的?
            

            2.3 位段的跨平台问题

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

            2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机

              器会出问题。

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

            4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是

              舍弃剩余的位还是利用,这是不确定的。

            总结:

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

            2.4 位段的应用

            3. 枚举

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

            把可能的取值一一列举。

            比如我们现实生活中:

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

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

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

            这里就可以使用枚举了。

            3.1 枚举类型的定义

            枚举的可能取值,默认是从0开始的,递增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

            4.2 联合的特点

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

            union Un
            {char c;//1
            	int i;//4
            };
            int main()
            {union Un u;
            	printf("%d\n", sizeof(u));
            	printf("%p\n", &u);
            	printf("%p\n", &(u.i));
            	printf("%p\n", &(u.c));
            	return 0;
            }
            

            代码结果:

            图片讲解:

            共用体的成员是共用同一块空间的

            来一道笔试题

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

            这是之前求大小端的图讲,现在将用联合体求大小端

            union Un
            {char c;//1
            	int i;//4
            };
            /int main()
            {union Un u;
            	u.i = 1;
            	if (u.c == 1)
            		printf("小端\n");
            	else
            		printf("大端\n");
            	return 0;
            }
            

            4.3 联合大小的计算

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

              比如:

              union Un1
              {char c[5];
              	int i;
              };
              union Un2
              {short c[7];
              	int i;
              };
              //下面输出的结果是什么?
              printf("%d\n", sizeof(union Un1));//8
              printf("%d\n", sizeof(union Un2));//16
              

              如果这份博客对大家有帮助,希望各位给恒川一个免费的点赞作为鼓励,并评论收藏一下,谢谢大家!!!

              制作不易,如果大家有什么疑问或给恒川的意见,欢迎评论区留言。