C语言中的自定义类型包括:数组、结构体、联合、枚举。自定义类型就是根据一定的规则自己定义数据类型。
1.声明结构体类型
1.1结构体的声明
结构体是一些值的集合,这些值称为成员变量,结构体的每个成员可以是不同数据类型的变量。
结构体声明的范式:

结构体声明的例子:
cpp
struct student
{
char name[20];
int age;
char sex[10];
char id[20];
}student1, student2;
这里就声明了一个student的结构体,结构名是student,其中name、age、sex和id是成员变量,后面的student1,student2是student数据类型的变量(注意:在这里声明的student类型的变量一定是全局变量)。
1.2结构体变量的创建和初始化
结构体变量的创建有两种方式:第一,在声明结构体的最后创建结构体变量,变量名写在结构体声明的最后一个大括号的后面分号的前面。第二,在定义完结构体后通过struct 结构体名 变量名;的格式创建一个结构体类型的变量。结构体类型变量的数据类型是struct 结构体名。
示例代码:
cpp
struct student
{
char name[20];
int age;
char sex[10];
char id[20];
}student1, student2;/*声明结构体的最后创建结构体变量*/
int main()
{
struct student student3;
/*定义完结构体后通过struct 结构体名 变量名;的格式创建一个结构体类型的变量*/
return 0;
}
结构体类型变量student1,student2,student3的数据类型均是struct student。
1.3结构体变量的初始化
结构体变量的初始化的方式也有两种:第一,按照结构体成员的顺序初始化。第二,按照指定的顺序初始化,按照 .成员变量 = 成员变量值 的方式赋值。
示例代码:
cpp
struct student
{
char name[20];
int age;
char sex[10];
char id[20];
}student1, student2;/*声明结构体的最后创建结构体变量*/
int main()
{
//按照结构体成员的顺序初始化
struct student student3 = { "李四", 18, "男", "20260127001" };
printf("name: %s\n", student3.name);
printf("age : %d\n", student3.age);
printf("sex : %s\n", student3.sex);
printf("id : %s\n", student3.id);
//按照指定的顺序初始化
struct student student4 = { .age = 19, .name = "张三", .id = "20260127002", .sex = "女" };
printf("name: %s\n", student4.name);
printf("age : %d\n", student4.age);
printf("sex : %s\n", student4.sex);
printf("id : %s\n", student4.id);
return 0;
}
1.4结构体中成员变量的访问
结构体成员访问的方式有三种:第一,结构体变量通过点操作符,结构体名.成员名即可访问该结构体成员。第二,通过结构体指针变量进行访问,结构体指针通过结构体成员访问操作符->访问结构体成员,形式是结构体指针->成员名;。第三,可以先将结构体指针变量解引用成为结构体变量再通过点操作符访问。
示例代码:
cpp
struct Stu
{
char name[30];
int age;
};
void test01(struct Stu* ss)
{
/*结构体指针先解引用再通过.操作符访问*/
printf("%s\n", (*ss).name);
printf("%d\n", (*ss).age);
/*结构体指针直接通过->操作符访问*/
printf("%s\n", ss->name);
printf("%d\n", ss->age);
}
int main()
{
struct Stu ss = { "zhangsan",20 };
/*直接通过.操作符访问*/
printf("%s\n", ss.name);
printf("%s\n", ss.name);
test01(&ss);
}
1.5结构体的特殊声明
在声明结构体时可以不完全声明,比如匿名结构体:声明的结构体没有结构名。匿名结构体只能在声明时创建结构体变量,声明之后就不能使用这个类型再创建变量,因为它没有名字。
示例代码:
cpp
struct
{
int a;
char b;
float c;
}x;
成员变量相同的两个匿名结构体本质还是不同的:

注意:一般情况下匿名结构体只能在声明时创建结构体变量,但是可以通过typedef重命名的方式使匿名结构体声明之后还可以创建该类型的变量。但是这多少有点脱裤子放屁的感觉。
1.6结构体的自引用
结构体的自引用就是在结构体中包含一个类型为该结构体的成员变量。
结构体自引用的应用场景:单链表。数组在内存中是连续存放的,如果有一串数字在内存中是离散的存放的,只知道这串数字中第一个数字的地址,需要顺着第一个数字的地址找到后面一连串的数字,在实现的过程中每一个数字可以看成一个结构体类型的变量,声明的结构体中包含两个成员变量:第一个成员变量表示数字的真实值,第二个成员变量需要是该结构体类型的指针,指向下一个结构体类型的元素所在的地址,这就是单链表。单链表中每一个结构体类型的变量称作节点,每个节点中保存着本节点的数据和指向下一个节点的指针,保存着本节点的数据的变量称为数据域,保存指向下一个节点的指针的变量称为指针域。
关于单链表中结构体声明的探讨:
解决方法:将下一个节点的地址存放到第二个成员变量中,因为指针的大小是固定的。
将Node重命名后续创建结构体变量时会更加方便。
2.结构体内存对齐
观察下面这段计算结构体大小的代码:

上述代码说明结构体中的成员变量在内存中并不是一个挨着一个的存储的,在存放时可能存在对齐。
补充:标准库中提供的宏:offsetof();
2.1结构体内存对齐规则
结构体变量创建时内存的申请规则:内存对齐的规则:
1.结构体的第一个成员必须存放到相对于结构体变量起始位置偏移量为0的地址处。
2.其他成员变量的起始地址位于某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。VS中对齐数默认值为8,Linux中gcc没有对齐数,对齐数就是成员自身大小。
3.结构体的总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中的最大值)的整数倍。
4.对于嵌套结构体的情况,嵌套的结构体成员的起始地址位于它自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
一般的结构体变量中成员变量的存储过程示例:

嵌套结构体变量中成员变量的存储过程示例:

2.2内存对齐的原因
通过上面的结构体类型的变量在内存中存放的过程可以看出来结构体中成员变量的存储是需要浪费一些空间的。实质上结构体的内存对齐是在以空间换时间。对于同样的数据若存储在未对齐的内存空间中处理器需要进行两次访存,而存储在对齐的内存空间中只需要一次访存。比如处理器总是一次从内存中存或取八个字节,那么地址必须是8的倍数,对于double类型的数据如果地址都是8的倍数取一次就能取完8个字节,如果起始地址不是8的倍数,需要取两次才能取完这8个字节,因为数据被放在两个8字节的内存块中。
如果既要内存对齐又要节省空间可以将占用空间小的成员变量集中在一块。

2.3如何修改默认对齐数?
如果觉得默认对齐数不合适可以使用#pragma pack( num )来设置编译器的对齐数,num是新设置的对齐数,对齐数一般设置为2的整数次方。如果要取消之前设置的对齐数还原成默认值可以使用#pragma pack( )命令还原成默认。

3.结构体传参
结构体传参有两种方式:①结构体类型的变量直接作为参数进行传递。②结构体类型变量的地址作为参数进行传递。
cpp
struct S
{
int data[100];
int num;
};
void test_printf1(struct S s)
{
printf("%d\n", s.num);
for (int i = 0; i < sizeof(s.data) / sizeof(s.data[0]); i++)
{
printf("%d ", s.data[i]);
}
printf("\n");
}
void test_printf2(const struct S *s)
{
printf("%d\n", s->num);
for (int i = 0; i < sizeof(s->data) / sizeof(s->data[0]); i++)
{
printf("%d ", s->data[i]);
}
}
int main()
{
struct S s1 = { {1,2,3,4,5},1 };
struct S s2 = { {1,2,3,4,5},2 };
test_printf1(s1);
test_printf2(&s2);
return 0;
}
一般结构体传参时传的是结构体变量的地址而不是直接将结构体传过去,因为函数传参时参数需要压栈,如果直接传结构体类型的变量可能由于结构体太大,传参时的系统中时间空间开销较大,导致程序性能下降。
如果参数类型直接是结构体,会在函数的栈帧中创建一块空间存放结构体中的各个值,函数调用结束后栈帧会被回收不会修改原始结构体中存放的值,但是如果传入指针可能会对原始结构体中的数据造成影响,因此需要在函数的参数部分加上const修饰。
4.结构体实现位段
4.1什么是位段
位段是基于结构体的,位段的声明和结构体相似,但是有两种区别:①位段中的成员变量只能是int、unsigned int、signed int、char类型。②位段的每个成员变量名后都跟着一个冒号与一个数字。
示例代码:

4.2位段的内存分配规则
位段中的成员变量只能是int、unsigned int、signed int、char类型,因此位段空间的开辟规则是空间以4字节或1字节的量度进行分配,首先分配4个字节或1个字节,当存储到某个变量时空间不够了,需要再分配4个字节或1个字节。位段是不跨平台的因此有许多不确定的因素,对于可移植的程序尽量避免使用位段。
分配过程解析:

4.3位段存在的问题
①int类型在早期16位机器上占2个字节,在现在常见的机器上(32位或64位)int占4个字节。因为标准尚未定义,不同的平台可能有不同的实现。
②int型数据被当成有符号整数还是无符号整数是不确定的。
4.4位段的应用
网络协议中IP数据报的格式中有很多的属性,每个属性中数据的范围并不会太大,有的属性只需几个比特位就能实现,如果还采用int申请4个字节的空间会造成太多浪费,因此可以采用位段的方式来实现不同属性对空间的不同要求,从而减轻网络的负载。

4.5位段注意事项
对于一个位段可能里面的几个成员变量公用一个字节,所以有些成员变量的起始位置并不是每个字节的起始位置,由于内存中的每个字节分配一个地址一个字节内部的每个比特位是没有地址的,那么这些变量的起始位置是没有地址的。因此不能对位段的成员变量使用&操作符,这会导致不能使用scanf直接对位段中的成员变量进行值的输入,只能是先将值赋给某个变量再将这个变量赋给位段的成员。
示例代码:

5.联合体
5.1联合体的声明
联合体和结构体类似也是由一个或多个成员组成,这些成员可以是不同的数据类型。
联合体与结构体不同的是在内存的分配上面,编译器只为联合体的最大成员分配足够的空间,联合体的特点是所有的成员共用同一片内存空间。给联合体其中一个成员赋值,其他成员的值也跟着变化。
示例代码:

5.2联合体的内存分配和使用特点
①联合体的内存分配规则
联合体所占空间的大小至少是最大成员的大小。当最大成员的大小不是最大对齐数的整数倍的时候,就要提升到最大对齐数的整数倍。
示例代码:

注意:如果成员是数组的话,成员的对齐数是针对数组中每个元素类型的大小来计算的,成员的大小是整个数组所占的大小。
②联合体的内存使用规则
联合体的所有成员是共用同一块内存空间的。给联合体其中一个成员赋值,其他成员的值也跟着变化。联合体的成员不能同时使用。
示例代码①

示例代码②

5.3联合体的应用
应用①:使用联合体可以用来节省空间。
对于一些物品比如:杯子、面包、水果,都有一些共性:价格,生产日期,库存量。但是又有一些个性:杯子有容量、最高温度,面包有硬度、甜度,水果有水分,新鲜度。
如果只用一个结构体来存储的话,成员变量需要包含所有的共性和特性,声明杯子型的变量时会浪费另外两种类型特有的成员变量所占用的空间。
这里可以采用结构体中嵌套联合体的方式,将每个物品特有的属性添加到联合体的成员变量中,因为联合体中的成员变量会共用一片内存空间。
示例代码:
cpp
struct St
{
int price;
char data[20];
int sum;
union
{
struct
{
int vol;
int tempreture;
}cup;
struct
{
int hard;
int sweat;
}bread;
struct
{
int water;
int fresh;
}fruit;
}item;
};
应用②:利用联合体判断大端存储还是小端存储。
cpp
int check()
{
union Un
{
int a;
char c;
};
union Un un;
un.a = 1;
if (un.a == un.c) return 1;/*小端存储*/
else return 0;/*大端存储*/
}
int main()
{
if (check) printf("小端存储\n");
else printf("大端存储\n");
return 0;
}
6.枚举
6.1枚举的声明
枚举就是一一列举,将所有可能的取值一一列举出来。生活中一些东西可以一一列举出来,比如周一到周日,三原色RGB等。生活中可以一一列举出来的东西可以用枚举类型来表示。
枚举类型和结构体的声明类似,不同的是枚举声明时大括号里面列举的是枚举的所有可能取值,中间用逗号隔开。这些可能取值是常量叫做枚举常量,可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
示例代码:
cpp
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex
{
man,
women
};
enum Color//颜⾊
{
RED = 2,
GREEN = 4,
BLUE //BLUE = 5
};
6.2枚举变量的定义和赋值
枚举变量赋值时只能赋枚举中的常量,不能将数字直接赋值。

注意:在C语言中可以给枚举变量直接赋值数字,但是在C++是不行的,C++的类型检查严格。
6.3枚举的优点
1.枚举的主要作用是将一种类型的可能常量值和数字联系起来。相比于#define而言,枚举变量是有类型的是枚举型的数据。
2.使用枚举可以调试,但是#define的代码在预处理时候会被直接删除。
3.枚举可以一次性定义多个常量。
4.枚举是有作用域的,声明在函数中,只能在函数中才能使用。





