前言:自定义类型和内置类型
char,int,long,float,double都叫做C语言的内置类型
但是当表达一个人,一本书,一件事情的时候,我们就需要一个复杂的类型,这种复杂的类型,一般就被叫做自定义类型
结构体
结构体类型的声明
1.结构是一些值的集合,这些值被称为成员变量,结构的每个成员可以是不同类型的变量
2.结构体的声明:
通式:struct tag //struct是结构体标签
{
member - list;//成员变量
}variable-list;
举例:
在结构体类型的声明中,定义的变量可以不需要初始化
3.用结构体创建变量以及结构体传参
还是用上面的Stu的结构体变量
由于这两个变量是在main函数中进行创建的,所以被叫做局部变量,当然结构体不仅仅可以创建局部变量,还可以创建全局变量
例如在main函数外面创建的变量以及直接在结构体声明部分创建的变量都被叫做这个程序的全局变量
即s3,4,5,6都被叫做全局变量
4.匿名结构体类型
①单个匿名结构体
在创建结构体的时候,可以不需要给结构体名字,但是必须在声明结构体的同时创建一个变量,如此时的sa
②多个匿名结构体
如果代码这样写是正确的吗?
结果是错误的,在编译器编译两个结构体的声明时,即使两个结构体的成员变量都是一样的,但是编译器会把这两个结构体当做两个不同的变量进行使用,所以在psa = &sa中,编译器认为psa和sa的类型不一样,就好比于把char类型的变量赋值int类型
结构体的自引用
这种类型是不正确的,因为sizeof(struct Node)是计算不出来的,无限套娃,所以结构体内是不可以包含结构体自己的类型的
这样的写法是正确的,由于下面那个结构体指针是一种指针,指针永远都是4/8个字节,所以这个结构体的sizeof是可以进行计算的
并且这种代可以模拟数据结构中的链表结构(假如有一串字符串123456,链表结构就是找到1就可以找到2,找到2就可以找到3…)
typedef重命名结构体
这样的结构体的声明等于把struct和Node整体放在一起重新取个名字叫Node
结构体变量的定义和初始化
1.单个结构体变量的定义和初始化
注意:结构体变量的定义需要利用大括号
注意指针类型的结构体成员不可以用”.“来进行引用,而必须使用”->“这个符号进行引用成员
2.嵌套结构体变量的定义和初始化
注意:每个结构体变量对应都需要一个{}
结构体内存对齐
1.结构体内存对齐的概念:
思考这个代码两个printf打印的结果
答案:12 8
这个时候就引入了结构体大小的概念
2.结构体的对齐规则
①结构体的第一个成员在与结构体变量偏移量为0的地址处
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数:编译器默认的一个对齐数与该成员大小的较小值(VS默认的对齐数为8,gcc编译器是没有默认对齐数的,可以理解为gcc编译器的默认对齐数为0)
③结构体的总大小为最大对齐数的整数倍
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
练习例题:
答案:16
3.为什么需要结构体的内存对齐
内存对齐实际上是用空间换时间的做法,所以C语言的执行效率是最高的
根据内存对齐的原则,结构体的创建一般会浪费大量的空间,那么我们应该怎么样做可以尽可能的让浪费的时间变少呢?
——让占用空间小的变量尽量集中在一起
4.重置编译器默认对齐数
#pragma pack()
ps:这个预处理在程序中可以出现两次,第二次出现不加入参数就是取消设置的默认对齐数
修改了默认对齐数之后,中间浪费的字节就会从7变成3
结论:当结构在对齐方式不合适的时候,可以自己设置对齐数,一般设置对齐数都是2的次方数(2,4,8,16…)
5.百度笔试题——offsetof宏
题目:······写一个宏,计算结构体中某变量相对于首地址的便宜,并给出说明
offsetof是用来计算结构体成员相对于结构体起始位置的偏移量的宏
头文件#include <stddef.h>
返回值就是偏移量的大小
第一个参数是结构体的类型名,第二个参数是要查找偏移量的结构体成员
ps: offset:偏移量 of:…的
offsetof:…的偏移量
七.结构体传参
在函数内部想改变函数外部的变量,就必须给函数传参地址(指针)
所以在Init函数中,需要在Init函数初始化结构体(需要对结构体成员进行改变),那么就需要传参地址,但是Print函数不需要对结构体成员进行改变,所以不需要传地址也可以完成代码(此时取地址也可以但是没必要)
即需要用&取地址才可以完成传参
位段
前言:结构体实现位段的能力,位段也是结构体的一种类型
位段的声明和结构是类似的,有两个不同
a.位段的成员必须是int,unsigned int或者signed int(只要是整形就可以了)
b.位段的成员名后边有一个冒号和一个数字
位段的基本形式
这个时候就定义了一个位段
位段的含义:
int a :2 表示a只需要两个比特位
位段的内存分配
位段的空间上是按照需要4个字节(int)或者1个字节(char)开辟的
所以位段不可以大于32(不可以大于一个整形的空间)
位段的大小
最后求出来的结果为:8(字节)
2+5+10+30 = 47(比特位)
代码解释:由内存的开辟规则:刚开始计算机会直接开辟一个整形的空间,也就是4个字节(32个比特位),a有2个比特位,b有5个比特位,c有10个比特位,此时剩下15个比特位,d不够放了。在编译器中,会直接浪费15个比特位,单独再为d开辟一个int的大小(32个比特位),d直接在新开辟的内存中用30个比特位,剩下的2个比特位的内存也会被浪费,所以最后开辟的为两个int的大小即8个字节
总体而言:位段就是为了节省空间
如果此时不使用位段就会开辟4个字节,但是用了位段后就开辟了2个字节就可以了
位段的截断
位段的存储也是从内存的右边往左边进行存储
位段的截取是从二进制序列的右边开始往左边进行截断
代码解释:
此时10的二进制序列为1010,但是因为char a:3所以a里面只放了010序列
20的二进制序列为10100,此时取0100序列放入b中
3的二进制序列为011,但是c有五个比特位,c中此时放的应该是00011序列
4的二进制序列为100,但是d有四个比特位,d中此时放的应该是0100序列
注意:
位段的存储是从内存的右边往左边进行存储的,所以在内存中将二进制序列转换成十六进制的时候,也许要从右往左看,例如:0010 为 2
0100 为 4
位段的跨平台问题
①int位段被当做有符号数还是无符号数不确定
②位段中最大位的数目不确定(16位机器最大16,32位及其最大32)
③位段中的成员是从左到右还是从右到左不确定,c语言没有给出明确的定义
④是否浪费比特位,是丢弃还是继续使用也不确定
位段的应用
例如:
上网的时候会进行数据的传输,进行数据包的传递,我们在聊天的时候聊天打的字还需要传输其他的信息,所以需要打包为数据包
枚举
前言:枚举顾名思义就是一一列举,把可能的取值一一列举,比如在我们的生活中:一周的星期一到星期日可以一一列举,性别也可以,月份也可以进行列举,这种可以被列举的被称为枚举类型
枚举的基本用法
1.枚举类型的定义
enum:关键字
枚举的引用
enum Sex
{
//枚举的可能取值
male,
female,
secret
}
枚举的可能取值一定是个常量,所以被叫做枚举常量
枚举的成员默认从0开始,如果不合适的话也可以像:RED = 2 一样的进行赋值,而不是修改;假如后面的GREEN,BLUE不进行赋值,那么在枚举表里面的下标就会从2开始变成2,3,4
如果只给GREEN赋值为9,那么这三个代表的就会是0,9,10
枚举常量的赋值只能在enum里面进行赋值
枚举的优点
为什么我们要用枚举而不是使用#define呢?
联合
联合类型的定义
联合也是一种特殊的自定义空间
打印的结果为4,为什么呢?
下面笔记会进行讲解原因
联合体的内存分布
由此看出,在联合体中,所有的元素以及整个联合体的地址都是一样的,所以他们共用了同一块空间,所以联合体也叫做共用体
并且枚举类型每个元素一定都是int类型的,是不可以自己指定的
联合的特点
①联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
②由于联合体中的都是一个内存空间,所以改其中任何一个变量都会改变整个成员
面试题:判断当前机器的大小端
先回忆大小端的存储模式:
int a = 0x11223344
所以只需要判断四个字节中的第一个字节存放的数据就可以判断是大端还是小端存储
写法一:利用强制类型转化的字符指针
写法二:利用联合的方法
(这里只写了函数内部,main函数和方法一相同)
联合大小的计算
①联合的大小至少是最大成员的大小
②当最大成员不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍处