C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型

作者的码云地址:https://gitee.com/dongtiao-xiewei
后续作者会更新力扣的每日一题系列,原代码会全部上传码云,推荐关注哦,笔芯~
还像更深入地了解c语言?快来订阅作者的c语言进阶专栏!作者承诺本系列不会TJ!预计更新:指针,字符串处理,内存管理,结构体,预处理等等

自定义变量
  • 引言
  • 结构体
    • 结构体声明
      • 匿名结构体类型
    • 结构体自引用
    • 结构体的定义与初始化
    • 结构体内存对齐
    • 结构体传参
    • 位段
  • 枚举
    • 枚举的定义
    • 枚举的取值
    • 枚举的应用
  • 联合体
    • 联合体定义
    • 联合的特点
    • 联合体大小的计算

引言 嗨喽小伙伴们我又来啦!冲冲冲!
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

在我们刚开始学C语言,还是个C语言萌新时候,想必已经对结构类型这四个字已经耳朵都快听出茧子了吧!
为了让计算机能够控制各种各样的数据,C语言中引入了像char,short,int,float,等基本数据类型。使我们能够控制一些简单的数据。
但是,有没有想过,有些复杂的数据类型,不能单纯的由一个基本数据类型描述的,该怎么办?
比如,我们要描述一个学生
变量名 基本数据类型
姓名 char
年龄 int
性别 char
成绩 float
亦或者,我们要描述一本书的信息
变量名 基本数据类型
书名 char
价格 float
作者 char
出版社 char
我们可以观察到,这些信息只用一个基本数据类型是远远不能描述的,所以,为了解决这个问题,我们引入了自定义类型
而解决以上的问题,用到结构体就行了
结构体 结构体是一堆值的集合,这些值被称为成员变量
我们可以在结构体类型中封装一系列变量,方便描述我们在引例中给出的复杂类型
结构体声明 它的声明通用方式是
struct tag//结构体类型名 { //member //这里定义成员变量 }; //分号千万不能丢

可以紧跟在分号后面创建变量,创建的为全局变量,也可以直接在此进行初始化
{ ... }s1,s2; //创建方式

例如,我们将引例中的例子来用结构体说明
//学生类型 struct Stu { char name[20]; int age; char sex[5]; float score; };

//书信息类型 struct Book { float price; char name[20]; char author[20]; };

等等。
但是下面有一种不寻常的声明方式
匿名结构体类型
声明的时候,可以省略结构体的名字
struct//这里的tag被省略了 { int a; int b; }x;

因为省略了结构体类型名,所以我们不能在主函数中定义结构体变量了
在声明后面定义也是唯一的方式
结构体自引用 我们应该都接触过链表这种数据结构,它在内存中不是连续存放的,但它们之间有一种逻辑关系,可以把它们联系在一起
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

有没有想过,它们是如何联系起来的呢?
【C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型】这样定义链表可以吗?
struct ListNode { int data; struct ListNode next; };

萌新可能比较容易理解为,一个链表先是一个数据,而为了存放下一个链表,在结构体中再定义一个结点结构体,方便查找下一个结点
如果这样可行的话,那这个你怎么算?
sizeof(struct ListNode)
很显然算不出来,因为在一个结构体中再定义一个自己,自己套自己,将会无限套娃下去
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

所以这个方法显然不可行
那么,如何实现正确的自引用呢?
正确的自引用方式
可以考虑使用地址,还是拿链表举例
struct ListNode { int data; struct ListNode* next; };

它现在在内存中的存储就是这样的了
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

结构体的定义与初始化 定义:全局定义,局部定义
定义的类型名:struct tag
例如定义一个学生变量
struct Stu s1;

初始化:用大括号初始化,成员变量用逗号隔开
还是上面的学生类型
s1={"zhangsan",18,"man",99,5};

初始化也可以嵌套初始化
结构体内存对齐 这里我们主要讨论一个问题:如何计算结构体类型的大小
struct S1 { char a; char b; int c; }; struct S2 { char a; int b; char c; }; int main() { printf("%d,%d\n",sizeof(struct S1),sizeof(struct S2)); return 0; }

可能大家就猜测了,一个char1字节,一个int4字节,大小是不是就是6字节啊?
遗憾的是,并不是,不仅不是,这两个结构体大小都不一样
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

那怎么计算结构体变量大小的呢?
这里就需要用到结构体对齐了,直接先上结论再解释
  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
Linux中的默认值为4
  1. 结构体总大小为最大对齐数的整数倍。
这里画图为大家详细的解释一下
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

现在大家应该就会算结构体类型大小的吧?
那为什么会存在内存对齐现象呢?
大家可以重点记住,这是一种用空间换取时间的做法
为了节省空间,我们可以把小的成员变量定义在一起
默认对齐数也是可以修改的
#pragma pack()//括号内是默认对齐数,建议修改为2^n

结构体传参 结论:传参的时候尽量使用指针,因为传值调用需要压栈,会重新复制一份
结构体,导致性能的下降,而在函数内修改结构体的内容也必须用到指针
使用方法
void func(struct S2* ps) { ps->a=0//注意这里使用箭头,结构体为指针时的成员访问方式 }

位段 这里好多教材上都没有哦!
我们需要先明白几个概念:
  1. 这里的位,指的是二进制的位
  2. 位段可以同样使用结构体实现
  3. 位段的成员必须是int,unsigned int(标准的定义没有char类型,但经过实验,char类型也可实现位段)
位段的声明如下
struct Test { char a:3; char b:4; char c:5; char d:4; };

位段名后面必须跟一个冒号和数字
那么,位段是如何开辟的空间?
重点:位段后面的数字,表示这个变量需要占用的比特位
例如test结构体位段
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

如果我们尝试修改呢?
注意,这里同样会发生截断现象!
struct Test { char a : 3; char b : 4; char c : 5; char d : 4; }s; int main() { s.a = 10; s.b = 12; s.c = 3; s.d = 4; return 0; }

它在内存中的情况就应该是以下这样
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

通过调试,验证了我们的结论
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

位段设计出来,有什么作用呢?
可以节省空间
比如,我们的学生类型中的分数变量,我们发现分数永远不会超过100分,所以我们只需要7个比特位存储,而不需要占用4字节,也就是32比特位了
枚举 注意,枚举是一种常量类型!
它与define定义的宏,和const都属于常量
顾名思义,枚举类型就是把可能的取值,一一列举出来
枚举的定义 枚举的通用定义如下
//enum枚举关键字enum name//枚举类型名 { //member list枚举成员名 };

比如我们要定义一个星期
//enum为枚举类型关键字enum Day { MON, TUES, WED, THUR, FRI, SAT, SUN };

枚举的取值 默认第一个变量为0,一次递增1,当然可以在定义中赋予初值,后面的变量会继续以被赋予的值继续加一
enum Day { MON=1, TUES, WED, THUR, FRI, SAT, SUN };

枚举的应用 基本使用:
可以定义枚举类型的变量,注意!定义了便不能被修改!
enum Day day1=MON; //定义了一个day1的枚举类型常用,初始化为MON

应用
比如我们要实现一个计算器
写一个计算器的菜单
  1. add 2. sub
  2. mul 4. div
我们在代码的选项中可以这样写
switch (option) case 1: case 2: case 3: case 4:

用数字来代替选项,会带来许多不必要的麻烦,如菜单对照等
我们考虑可以使用枚举变量
enum Option { ADD=1, SUB, MUL, DIV };

将菜单改为以下这样,就方便我们进行对照了
switch (option) case ADD: case SUB: case MUL: case DIV:

联合体 它的特征是成员变量共用一块存储空间
联合体定义 与结构体定义类似
union Test { char c; int i; }

联合的特点 就是共用一块成员变量,我们可以写个简单的代码验证一下
union U { int i; char c; }u; int main() { u.i = 0x11223344; u.c = 0x55; printf("%x\n",u.i); return 0; }

输出结果为
C语言进阶系列|超详细的C进阶教程!(四)深度解剖C语言自定义类型
文章图片

联合体大小的计算 也存在内存对齐现象
  1. 至少为成员最大变量的大小,因为要确保能容纳下最大的变量
  2. 当最大成员大小不是最大对齐数整数倍的时候,要对齐到最大对齐数的整数倍
例如
union U { char c[5]; int i; }

因为最大对齐数是4(int),并不是最大变量5的整数倍(char[5])
所以对齐前是5字节,对齐后就是8字节

    推荐阅读