前言
在之前的学习中,我们学习过数组,数组用来存放一些相同类型的变量;那如果我们需要存储不同类型的数据,数组就无法满足我们的需求,所以,现在就来学习一下能够存放不同类型的变量--自定义类型
结构体
引言:
我们现在需要存放一名学生的信息,姓名、年龄、学号等,在之前,我们就需要创建多个变量来存储
#include<stdio.h> int main(){ char name[] = "zhangsan"; int age = 18; char id[]="2325813007"; return 0; }
如果学生信息有很多,我们就要创建更多的变量,现在我们使用结构体来存储学生的信息
struct Student { char name[20];//姓名 int age;//年龄 char id[10];//学号 }; int main(){ struct Student s1={"zhangsan",18,"2325813007"}; return 0; }
这里可以看到,确实把学生信息存储起来了。接下来,来详细学习一下结构体:
结构体类型
结构体声明:
struct tag { member-list; }variable-list;
struct是关键字,tag是结构体类型名,member-list是结构体类型变量(就是结构体中的所有变量),variable是结构体变量名(可以不写,写了就相当于在声明结构体类型时创建的结构体变量)。
结构体变量的创建和初始化:
声明完结构体类型,接下来创建并初始化结构体变量
struct Student //声明结构体变量 { char name[20];//姓名 int age;//年龄 char id[10];//学号 }; int main() { struct Student s1 = { "zhangsan",18,"202309107" }; //创建s1并初始化 printf("%s\n", s1.name); printf("%d\n", s1.age); printf("%s\n", s1.id); struct Student s2 = { "lisi",20,"202309106" }; //创建s2并初始化 printf("%s\n", s2.name); printf("%d\n", s2.age); printf("%s\n", s2.id); return 0; }
这里补充一下结构体访问成员操作符 . 和 ->
在上述代码中我们看到了 s1.age 这个访问的就是s1这个结构体变量中的age。
我们也可以使用->来访问结构体变量,但是需要注意 ->前面应该是结构体变量的地址
int main() { struct Student s1 = { "zhangsan",18,"202309107" }; struct Student* ps1 = &s1; printf("%s\n", ps1->name); printf("%d\n", ps1->age); printf("%s\n", ps1->id); struct Student s2 = { "lisi",20,"202309106" }; printf("%s\n", (&s2)->name); printf("%d\n", (&s2)->age); printf("%s\n", (&s2)->id); return 0; }
在这里要注意:使用 ->时,前面应该是结构体变量的地址,而不是结构体变量名
结构的自引用:
接下看这样一段代码:
struct Node { int data; struct Node next; };
这是应该错误的代码,因为一个结构体中再包含一个同类型的结构体变量,那这样的结构体变量大小会特别特别大,显然是不可以的。
那我们想要用结构体来实现链式访问又该如何去写呢?
可以这样,在结构体类型中存放下一个要访问的结构体变量的地址。
struct Node { int data; struct Node* next; };
这里涉及到一点链表的知识,数据结构中会学到。
到这里,我们会发现一个问题,每一次这样创建结构体变量是都要写 struct Node 感觉很麻烦,我们使用typedf 对匿名结构体类型重命名
typedef struct Node { int data; struct Node* next; }Node;
这样,在创建结构体变量时就只需要写 Node
typedef struct Node { int data; struct Node* next; }Node; int main(){ Node a2 = { 0 }; Node a1 = { 1, &a2 }; return 0; }
结构体内存对齐
到这里,应该已经掌握了结构体的基本使用
现在思考应该问题,结构体大小怎么计算呢?
现在也是比较热门的考点:结构体内存对齐
对齐规则:
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对其到某个数字(对齐数)的整数倍的地址数。
- 结构体大小的最大对齐数(结构体中每一个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
- 对齐数就是编译器默认的一个对齐数 与 成员变量大小的较小值
- VS中默认的值是8
- linux 中 gcc没有默认对齐数,对齐数就是成员自身的大小
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
看使用规则那肯定是看着很懵,接下来,自己计算一下结构体的大小:
struct S1 { char c1; int i; char c2; }; int main(){ printf("%d\n", sizeof(struct S1)); return 0; }
这里结构体大小是12,如何计算的呢?这里分析一下
接下来再看下面这个:
struct S2 { char c1; char c2; int i; };
这个结构体与上面那一个都是两个char类型一个int型,但是它们大小一样吗?
我们可以看到,这两个结构体大小不一样,我们来分析一下S2结构体的大小:
接下来看,结构体嵌套该如何去求大小?简单来说就是把结构体看成一个变量整体
struct S4 { char c1; struct S2 s; double d; };
来分析一下,
为什么要有内存对齐?
1.平台原因:
不是所有的硬件平台都可以访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次访问,而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分开放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
但是这样空间浪费有点多,我们既要满足对齐,又要节省空间,我们就需要让占用空间小的尽可能集中在一起,就如上面的S1和S2 ,S2的两个char类型的集中在一起,占用空间要比S1 小。
结构体传参
struct S { int data[1000]; int num; }; struct S s = { {1,2,3,4}, 1000 }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s); //传结构体 print2(&s); //传地址 return 0; }
这里print2函数要比较好 原因如下:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销
如果传递的一个结构体的时候,结构体特别大,参数压栈的系统开销就比较大,所以会导致性能的下降。
总而言之:结构体传参的时候,要传结构体的地址
结构体实现位段
位段的声明和结构是类似的,有两处不同
- 位段的成员必须是int、unsigned int或signed int(C99当中位段成员类型也可以选择其他类型)。
- 位段的成员名后面有一个冒号和一个数字。(数字就是成员所占bit位数)
例如这样:
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
位段的内存分配:
- 位段的成员可以是int 、unsinged int 、signed int 或者char 类型
- 位段的空间上是按照需要以4个字节或者1个字节的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可一职的程序应该避免使用位段。
接下来,来看一下位段到底是如何分配内存空间的?
//⼀个例⼦ struct S { char a:3; char b:4; char c:5; char d:4; }; int main(){ struct S s = {0}; s.a = 10; s.b = 12; s.c = 3; s.d = 4; //空间是如何开辟的? return 0; }
位段的跨平台问题:
- int位端对当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32;写成27,在16位机器就会出现问题
- 位段的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当结构体包含两个位段,第二个位段成员比较大,无法容纳在第一个位段的剩余位时,舍弃剩余位还是继续使用,这也是不确定的。
位段使用时注意:不能对位段成员使用&操作符,这样就不能使用scanf直接给位段成员输入值,只能先输入在一个变量中,然后赋值给位段成员。
联合体(共用体)
在数据在内存中存储中,用到了联合体,去验证大小端字节序,在这里详细讲解联合体
联合体同结构体一样,联合体也是由多个不同类型的变量构成(也可以是一个变量),
但是,与 结构体不同的是,编译器只会为最大的成员分配足够的空间,(这也是联合体的特点,所有成员共用同一块空间,也称为共用体),联合体关键字 union
给联合体其中一个成员赋值,其他成员的值也会跟着变化。
这里来看一下联合体的大小:
union Un { char c; int i; }; int main() { //联合变量的定义 union Un un = {0}; //计算连个变量的⼤⼩ printf("%d\n", sizeof(un)); return 0; }
联合体的特点:
联合体成员共用一块内存
#include <stdio.h> //联合类型的声明 union Un { char c; int i; }; int main() { //联合变量的定义 union Un un = {0}; printf("%p\n", &(un.i)); printf("%p\n", &(un.c)); printf("%p\n", &un); return 0; }
可以看到,对un.i un.c和un取地址的结果都是一样的。
对一个成员赋值,其他成员的值跟着变化
union Un { char c; int i; }; int main() { //联合变量的定义 union Un un = { 0 }; un.i = 0x11223344; un.c = 0x55; printf("%x\n", un.i); return 0; }
可以看到,对un.c赋值,un.i的值也会变化。
联合体的大小:
接下来,来探究一下联合体所占内存的大小
- 联合体的大小至少是最大成员的大小
- 当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍
来看一段代码:
union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; int main() { printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2)); return 0; }
看到这里,可能感觉到懵了,不是只给最大的成员开辟空间吗,为什么结果是这样的?我第一次看到这样代码结果时,也是很懵,但是理解了是怎样计算的它的大小就懂了,
联合体计算大小,当最大成员不是最大对齐数的整数倍就要对齐到最大对齐数的整数倍
这里就是怎样,Un1中c[5]大小为5,但对齐数char是1,int的对齐数是4,所以就要对齐到4 的整数倍,就是8。
联合体与结构体比较:
通过一个例子来对比一下联合体和结构体
现在,要搞一个活动,需要上线一个礼品兑换单,这个兑换单中有三个商品:图书、杯子、衬衫。而每一个商品都有:库存量、价格、商品类型等信息
图书:书名、作者、页数
杯子:设计
衬衫:设计、可选颜色、可选尺寸
用结构体来表示:
struct gift_list1 { //公共属性 int stock_number;//库存量 double price; //定价 int item_type;//商品类型 //特殊属性 char title[20];//书名 char author[20];//作者 int num_pages;//⻚数 char design[30];//设计 int colors;//颜⾊ int sizes;//尺⼨ };
这样看用结构体来表示,这样会占用很多空间,我们再来看一下联合体:
struct gift_list2 { int stock_number;//库存量 double price; //定价 int item_type;//商品类型 union { struct { char title[20];//书名 char author[20];//作者 int num_pages;//⻚数 }book; struct { char design[30];//设计 }mug; struct { char design[30];//设计 int colors;//颜⾊ int sizes;//尺⼨ }shirt; }item; };
我们来计算一下这两个的大小
可以看出,使用联合体确实要比结构体节省空间。
枚举类型
枚举顾名思义就是一一列举
就是把可能的取值一一列举出来
举个例子,生活中,星期一到星期日可以一一列举、性别也可以一一列举、月份也可以一一列举。
这些都可以使用枚举
enum Day {//星期几 Mon, Tues, Wed, Thur, Fri, Sat, Sun }; enum Sex//性别 { MALE, FEMALE, SECRET }; enum Color//颜⾊ { RED, GREEN, BLUE };
上述这些都是枚举类型{}内的内容就是枚举类型可能的取值,也叫做枚举常量
这些可能取值都是有值的,默认从0开始,一次递增1,当然也可以声明时对其进行赋初值
enum Color//颜⾊ { RED=2, GREEN=4, BLUE=8 };
学习到这里,我们会感觉到枚举常量很多余,我们可以使用#define定义常量,为什么非要使用枚举?
枚举常量优点:
- 可以增加代码的可读性和可维护性
- 相对于#idefine定义的标识符,枚举类型有类型检查,更加谨慎
- 便于调试,预处理阶段会删除#define定义的符号
- 使用方便,一次可以定义多个变量
- 枚举常量是遵循作用域规则的,枚举声明在函数内,就只能在函数内使用
enum Day {//星期几 Mon, Tues=10, Wed, Thur=20, Fri, Sat=30, Sun }; int main() { enum Day today = Mon; printf("%d\n", today++); printf("%d\n", Tues); printf("%d\n", Wed); printf("%d\n", Thur); printf("%d\n", Fri); printf("%d\n", Sat); printf("%d\n", Sun); return 0; }
感谢观看,希望一下内容对你有所帮助,如果内容对你有作用,可以一键三连加关注,作者也正在学习中,有错误的地方还请指出,感谢!!!