WHCSRL 技术网

【C语言】☀️自定义类型(结构+位段+枚举+联合)【推荐集合】

目录

一、结构体

为什么会有结构体呢?

结构体的声明

结构的基础知识

结构体的声明

结构体的自引用

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

结构体成员的访问

1.  结构体对象.结构体成员

 2.结构体指针->结构体成员

结构体传参 

结构体内存对齐

 对齐规则

为什么存在内存对齐?

修改默认对齐数

二、位段

什么是位段

位段的内存分配

位段的跨平台问题

位段的应用

三、枚举常量

什么是枚举?

枚举类型的定义

值是多少?

枚举类型的优点

四、联合(共用体)

联合类型的定义

联合类型的声明


一、结构体

为什么会有结构体呢?

我们前面所学到的那些数据类型:char,int,double,还有指针都是不足以去表达对象,如果我们要去表示一个人,能用一个数字去表示吗?肯定是不行的,要知道人是属于复杂对象,不能简单的用某个数来表示,要表示一个人,需要很多方面,比如姓名,性别,年龄等。要表示人,我们就得创造一种复杂类型,C语言里面就有了结构体类型

结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型

结构体类型也是需要字己创建的

结构体的声明

结构的基础知识

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

结构体的声明

描述一个人:

方式1:

  1. struct Peo //声明了一个结构体类型
  2. {
  3. char name[20];
  4. int age;
  5. };

这是对结构体的声明,struct是结构体关键字,Peo是结构体标签(结构体类型名),花括号里面的是结构体成员变量

结构体成员可以是标量,数组,指针,结构体

 方式2:

  1. struct Peo
  2. {
  3. char name[20];
  4. int age;
  5. }p1, p2 //全局变量

方式2是在结构体声明的时候就创建了两个struct Peo类型的结构体变量,但要注意的是这种创建方式得到的p1,p2都是全局变量,作用域比较广,一般不建议这样写

方式3(匿名结构体类型)

  1. struct
  2. {
  3. char name[20];
  4. int age;
  5. }p1, p2;
  1. struct
  2. {
  3. char name[20];
  4. int age;
  5. }p1;
  6. struct
  7. {
  8. char name[20];
  9. int age;
  10. }*p;

第一个代码这里直接将结构体标签(结构体类型名)省略掉,这样写的话就只能在生命完结构体之后就创建p1,p2结构体变量,不能再在后面的main函数中创建,比较局限,不建议用

第二个代码在后面创建了指针p,但是如果在第二个代码的基础上,下面写p = &p1;这就是非法的操作,编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的

结构体的自引用

我们在用结构体的时候能不能在声明结构体的时候在结构体里面写这个结构体

如下代码:

  1. struct Peo
  2. {
  3. char name[20];
  4. int age;
  5. struct Peo next;
  6. };

要注意这样是不行的,因为如果这样写的话,那么这个结构体的大小应该是多少呢? 肯定就算不出来了

正确写法

  1. struct Peo
  2. {
  3. int data;
  4. struct Peo* next;
  5. };

在该结构体里面放同类型的结构体指针

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

如何定义结构体变量,请看如下代码:

代码1:

  1. #include <stdio.h>
  2. #include <string.h>
  3. struct Peo
  4. {
  5. char name[20];
  6. int age;
  7. };
  8. int main()
  9. {
  10. struct Peo p = { "张三",18 }; //通过结构体类型来创建结构体变量并初始化
  11. printf("%%%%s %%%%d ", p.name, p.age);
  12. //修改里面的内容
  13. strcpy(p.name, "李四");
  14. p.age = 20;
  15. printf("%%%%s %%%%d ", p.name, p.age);
  16. return 0;
  17. }

通过这段代码创建了一个描述人的结构体并初始化,在后面对它里面的内容进行了修改

类比:

其实使用结构体就相当于是我们盖房子,前面的声明结构体就是画的图纸,后面的创建结构体变量就是照着图纸盖房子

所以我们在声明结构体的时候系统是不会给它分配空间的,只有在创建了结构体变量之后系统才会给他分配空间

代码2(创建出全局结构体变量)

  1. struct Peo
  2. {
  3. char name[20];
  4. int age;
  5. }p1,p2,p3;

在结构体声明的时候直接就在花括号后面写上需要创建的结构体变量名/标签,但要注意这里创建的是全局变量建议少用这种创建 方式

结构体成员的访问

1.  结构体对象.结构体成员

如下代码,通过结构体变量名/标签对结构体成员进行访问

  1. struct Peo
  2. {
  3. char name[20];
  4. int age;
  5. };
  6. int main()
  7. {
  8. struct Peo p = { "张三",18 };
  9. printf("%%%%s %%%%d ", p.name, p.age);
  10. return 0;
  11. }

运行结果:

 2.结构体指针->结构体成员

 如下代码:通过结构体指针来访问结构体成员

  1. #include <stdio.h>
  2. struct Peo
  3. {
  4. char name[20];
  5. int age;
  6. };
  7. int main()
  8. {
  9. struct Peo p = { "张三",18 };
  10. struct Peo* ps = &p;
  11. printf("%%%%s %%%%d ", ps->name, ps->age);
  12. return 0;
  13. }

运行结果:

结构体传参 

 结构体传参分为传值传地址

1.传值

如下代码:

  1. struct Peo
  2. {
  3. char name[20];
  4. int age;
  5. };
  6. void print1(struct Peo p)
  7. {
  8. printf("%%%%s %%%%d ", p.name, p.age);
  9. }
  10. int main()
  11. {
  12. struct Peo p = { "张三",18 };
  13. print1(p); //传值,传整个结构体过去
  14. return 0;
  15. }

这是直接将整个结构体传了过去

2.传址

  1. struct Peo
  2. {
  3. char name[20];
  4. int age;
  5. };
  6. void print2(struct Peo* ps)
  7. {
  8. printf("%%%%s %%%%d ", ps->name, ps->age);
  9. }
  10. int main()
  11. {
  12. struct Peo p = { "张三",18 };
  13. print2(&p);
  14. return 0;
  15. }

将结构体的地址传过去

这两种传参方法我们首选第二种方法

原因就是:

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

结构体内存对齐

如果我们要计算一个结构体的内存大小,该怎么算呢?  是直接将所有的成员变量的内存大小相加吗?

首先请看如下代码:

  1. #include <stdio.h>
  2. struct P
  3. {
  4. char a;
  5. int b;
  6. };
  7. int main()
  8. {
  9. struct P p;
  10. printf("%%%%d ", sizeof(p));
  11. }

 这里创建结构体变量p之后求这个结构体的大小,是1+4==5吗

运行结果:

 打印结果是8,那显然说明结构体的大小并不是简单的成员变量相加,那么这个8是怎么来的呢?这就涉及到了结构体内存对齐

 对齐规则

  1.  第一个成员在与结构体变量偏移量为0的地址处(起始位置的0偏移量处)
  2. 剩下的其他成员变量要对齐到某个数字(对齐数)的整数倍处
  3. 对齐数:每个成员自身的大小和所用编译器的默认对齐数的较小值
  4. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  5. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

提示:VS编译器的默认对齐数是8,linux下的gcc编译器无默认对齐数

那么现在就可以利用对齐规则来求结构体大小了

上面的代码中,结构体中第一个为char类型,直接放在0偏移量处

第二个类型为int类型,自身大小为4,默认对齐数为8,所以对齐数取较小值4

结构体总大小为最大对齐数的整数倍

通过画图来看

 所以这个结构体的大小就是8

为什么存在内存对齐?

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

所以结构体内存对齐就是拿空间来换取时间

那在设计结构体的时候,我们既要满足对齐,又要节省空间,让占用空间小的成员尽量集中在一起

修改默认对齐数

刚才我们在前面说过VS编译器的默认对齐数是8,但其实这个默认对齐数是可以修改的

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

#pragma pack()      //括号内放想达到的默认对齐数

如下代码:

  1. #include <stdio.h>
  2. #pragma pack(4) //修改默认对齐数为4
  3. struct P
  4. {
  5. char a;
  6. int b;
  7. };
  8. #pragma pack() //取消设置的默认对齐数,还原为默认值
  9. int main()
  10. {
  11. struct P p;
  12. printf("%%%%d ", sizeof(p));
  13. }

注意:这个默认对齐数不能随意修改,所修改的值必须是2^n  (n==0,1,2,3……)

二、位段

什么是位段

C语言允许在一个结构体以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据

如下:

  1. struct A
  2. {
  3. int a : 2;
  4. int b : 3;
  5. int c : 5;
  6. int d : 10;
  7. };

从代码可以看出位段的声明和结构体很类似,冒号后面的数字表示该成员的大小(单位bit),有两个不同:

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

那么这个位段的大小是多少呢? 是2+3+5+10个bit吗?肯定不是的,通过代码来测试一下

  1. struct A
  2. {
  3. int a : 2;
  4. int b : 3;
  5. int c : 30;
  6. int d : 10;
  7. };
  8. int main()
  9. {
  10. printf("%%%%d ", sizeof(struct A));
  11. return 0;
  12. }

运行结果:

  那么这12byte是怎么来的呢?      接下来看位段的内存分配

位段的内存分配

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

前面的代码中位段的声明中有4个int类型的变量,本来应该是要占用16个字节的空间的,但是在使用位段之后只占用了12个字节,计算方式如下:

 在申请的内存不够用,,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的,取决于编译器。

位段的跨平台问题

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

位段的应用

在我们平时要去发个hello消息给朋友的时候,不仅仅是发了hello,而是把hello和其他的数据捆绑在一起发送的,比如这消息是谁发的,发给谁,目标ip地址信息等,但是这些信息有的可能只需要几个bit就能存下,全部用int的话就太浪费了,这样也会使影响网络状况,这种情况就用位段

三、枚举常量

什么是枚举?

枚举就是一一列举的意思,把可能的取值都列举出来

比如颜色,星期,月份这些都是可以列举出来的,C语言中就将我们想要的某一类型的值定义成枚举类型,并且让他们的值也可以一一列举出来

枚举类型的定义

比如现在将三原色列举出来

  1. enum Color
  2. {
  3. RED,
  4. GREEN,
  5. BLUE
  6. };

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

值是多少?

那么这里面的值都分别是多少呢?  如下代码:

  1. enum Color
  2. {
  3. RED = 3,
  4. GREEN = 7,
  5. BLUE = 5
  6. };
  7. int main()
  8. {
  9. printf("%%%%d ", RED);
  10. printf("%%%%d ", GREEN);
  11. printf("%%%%d ", BLUE);
  12. }

打印结果:

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

  1. enum Color
  2. {
  3. RED = 3,
  4. GREEN = 7,
  5. BLUE = 5
  6. };

可以这么使用

  1. enum Color
  2. {
  3. RED ,
  4. GREEN,
  5. BLUE
  6. };
  7. int main()
  8. {
  9. enum Color c = RED;
  10. }

枚举类型的优点

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

四、联合(共用体)

联合类型的定义

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

联合类型的声明

如下代码:

  1. union U
  2. {
  3. char i;
  4. int a;
  5. };

那么这里的联合体大小是多少呢?

也是对齐数原则,稍有不同

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

总体大小是最大对齐数的整数倍

-----------------------------------------------------------------

-----------C语言浮点数在内存中的存储完结---------

关于C语言,每个知识点后面都会单独写博客更加详细的介绍

欢迎大家关注!!!

一起学习交流 !!!

让我们将编程进行到底!!!

--------------整理不易,请三连支持------------------

推荐阅读