我们在写变量的时候,像int 、float 、double 、char这些已经写死了,int类型里面不能再写char类型了,那么不够灵活,比如通讯录、学生信息管理系统这些小项目,难道每次使用什么信息就要创建什么类型的变量?
为了让表示 :身份,姓名,年龄.........这些信息更加灵活使用,我们来认识结构体
什么是结构体
在C中定义的一种数据类型,它可以将不同类型的数据变量组合在一起,形成一个整体
结构体可以存放多种数据类型,比如:学生姓名(char),年龄(int),身高(float).....
结构体的声明
比如:
我们来解释各个字符:
struct:表明这是一个结构体
student: 结构体名称(也叫“标记名”),自选自己定义
“{ }”:表明里面的内容是一个整体
“ ;”:表明这个结构体声明结束
“char Name、int Age、char Gender”:不同的变量集合(一个个变量组成)
结构体声明的位置
创建一个结构体,它的创建位置导致它发挥的作用域也不同
比如我创建在函数内部,那么它的作用域就只在这个函数内部:(声明在其它自定义函数同理)
如果在函数外面声明,那么它在这个文件是可以发挥作用的:
结构体的定义使用
struct student只是相当于类型,例如“int”“char”“double”这些类型
那么我们知道类型+变量名(+内容)才算完成一个变量的创建,编译器也才初始化内容,给这个变量开创空间
同理,结构体也是如此,我们的声明相当于创建了这样一个类型给编译器,如果想使用,还需要附加 变量名(这个可以理解为 int Age = 18;)
这里提一个简单快捷的声明使用方法:typedef
typedef:可以理解为将名字简化的一个操作,例如:
struct student 被typedef修饰后,再在分号前面重新写一下结构名,以后就可以写成:student
注意:结构体在使用时需要对准变量,什么意思呢?比如年龄就对应下面图中int类型的第二个变量,名字就对应图中char类型的第一个变量
如果不对齐,那么后续使用的时候会出错,这点很重要(结构体使用的时候一定要对应变量类型)
typedef与不加typedef在结构体中的命名意义不同
比如:
如果不加typedef,那么分号前面的 p2 就相当于已经创建了一个叫 p2 的结构体变量,可以直接用
如果加了typedef,分号前面的 p1 相当于结构体类型(p1 == struct student),再命名就可以使用了
结构体的3种打印方式
直接打印
注意区分占位符的种类,用结构体名字.变量名就可以打印出来(注意struct student 是类型)
传值打印(不建议)
就是将 结构体变量名 作为参数传给函数,再用结构体接收就行了
传址打印
注:(因为传值打印不推荐,性能不怎么好,所以我们一般选择传址打印)
传址打印:将结构体的地址传过去用结构体指针接收(因为是地址,所以要用指针接收)
2种传址方式打印:
1:指针解引用方式(注意:(*指针)拿到的是s1的地址,指针指向s1)
2:指针箭头指向方式
这2种打印方式的理解其实跟直接打印方式差不多,只是借助指针指向来找到结构体里面的元素
Const修饰 结构体 跟 结构体指针
const可以理解为一把锁,把内容锁定了,那么就不可以修改了
比如下面2种:(你只需要在想加的地方加上const就行了!理解也很简单,就是无法更改它修饰的内容)
这里需要注意的2点:(很重要)(注意 指针指向的地址 跟 指针指向内容 两者的区别)
如果是在 *指针 左边 加const,那么表示 指针指向的内容不能修改,但是指针指向的地址可以修改
如果是在 *指针 中间 加const,表明指针指向的地址不能修改,但是指针指向内容可以修改
结构体的特殊声明(从这里开始是进阶内容)
什么是结构体的特殊声明?
就是没有 结构体名称 ,只有struct,例如:
这种特殊声明的结构体,可以叫匿名结构体,只能使用一次,原因是什么?(参考下面的图)
第一次使用创建了一个这样的结构体变量
第二次使用就不能再创建这样的结构体变量了,因为地址发生了改变
(比如两个匿名结构体,只是结构体变量名字不同,但是系统会认为这是2个不同的结构体变量)
结构体的自引用
我们之前是不是有嵌套结构体的习惯,嵌套几个还好,但是如果要嵌套数量多了呢?如果还使用嵌套,那么就属于设计错误,不符合语法
我们先来看嵌套结构体:(一环套一环的)(这种写法以后慢慢改掉,养成好习惯!)
如果想结构体里面可以直接访问下一个结构体,那么我们有这么一个方法:
用 指针 ,指针的类型是下一个结构体的 类型,例如:
注意:最容易犯错误的地方就是 指针的类型(注意是下一个要指向的结构体类型)
结构体的存储
结构体的存储相较于其它存储方方式,有很大区别,比如:
int类型的+char类型的数组不是等于4+5=9吗?
我们来学习结构体的存储:
规则:
1:结构体的第一个成员,对齐到结构体在内存中的存放位置的0偏移处
2:从第二个成员开始,每个成员都要对齐到(一个对齐数)的整数倍处
3:结构体的总大小必须是所有成员的对齐数中最大对齐数的整数倍
看不懂没关系,先给大家翻译2个陌生词:
偏移处:(就是自身到起始地中间的字节数)
对齐数:(这个是数据存储时规定的,只要知道有这个名词就行了)
Vs编译器上规定默认对齐数为8 ,其它的对齐数题目会强调
我从题目中给大家讲解怎么计算:
我们先取一块内存空间,假设编号(地址)从0开始(所有找倍数的都是根据编号来找)
char c1 占一个字节,它是这个结构体的第一个变量,直接放到第0个编号处,也就是0的位置
char c2 占一个字节,它是第二个变量(规定从第二个变量开始,要对齐一个较小对齐数的整数倍),那么我们知道Vs的对齐数默认是 8 ,8跟1(这个1代表它占几个字节)比较,取较小值1,再在上一个变量存储的末尾开始(也就是从0编号的末尾开始),根据编号找1的倍数,如果是直接存放(直到找到为止)
int i 占4个字节,8跟4比较,取较小值4,再在上一个数据存放的末尾开始找4的倍数,找到后开始存放4个字节的大小
结构体的总大小:找结构体中变量类型最大的对齐数(我们假设是A),再从最后一个变量存储的末尾开始找A的最小倍数,这个倍数是几,结构体大小就是几
这是演示图:
总结:用编译器给的对齐数,跟变量类型占的字节大小比较,取较小值,再在编号上找这个较小值的倍数,再开始存放这个变量占的字节 个数
再根据所有变量中最大的对齐数,从最后一个变量存储的末尾找它的倍数,最小倍数是几,结构体大小就是几
我们来练习一题:
演示:
结构体偏移量的计算:offsetof 头文件:<stddef.h>
格式: offsetof(结构体类型,变量名)
例如:(可以跟上图比较)
为什么要存在对齐数
1:平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的,某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常
2:性能原因
为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问,仅需一次访问
总结:结构体的内存对齐是拿空间换取时间的做法
修改默认对齐数
格式: #pragma pack(数字 或者 什么都不放)
如果要恢复原来的编译器默认对齐数,则只需再次使用就行,括号里面 什么都不放
比如:
这样再次计算结构体,就要按新设置的对齐数来计算,比如:
这样就将之前的对齐数改为了1,结构体的大小也就从之前的 16 变成了 13