🐎前言
🐎🐎🐎🐎我们在完成一些多个信息的记录的时候往往需要定义多个变量,这样在使用这些变量的时候会显得很笨重,有没有一种类型可以包含多种类型,通过这一种类型可以使用包含多个类型的某一个变量呢,答案是有,我们接下来要介绍的结构体类型就可以完成这样的操作,同时还有其他的一些自定义类型会随着我们对代码的深入了解逐渐的发现它们的妙用,本文会通过下面的导图由浅入深的了解自定义类型。
🥭一、结构体 🥭
一个结构体就像是一个集合,这个集合中存放的信息就是结构体的成员变量,这些成员可以是不同或相同类型的变量,当我们声明一个结构体类型后,我们可以通过该结构体类型使用其中的变量。
🍁1.1结构体的声明🍁
一般声明:
通过struct关键字可以声明一个结构体类型,具体的方式参考如下:
struct Stu//结构体标签 { char name[20];//字符数组--姓名 int age; //整型的变量--年龄 };//注意这里有一个分号不可省略
匿名结构体类型:
在声明一个结构体类型时,结构体标签可以省略,此时声明的结构体就是不完全声明,该结构体的类型也叫匿名结构体类型,参考如下:
struct { int a; int b; char c; float d; }x;//此时分号前必须有一个变量作为一个结构体名
🍁1.2结构体的自引用🍁
结构体的自引用就是在声明结构体时,结构体内的成员包含结构体类型,被称为结构体的自引用。
1.错误代码;
struct S1 { int data; struct S1 next;//结构体类型的next };
对于上面的代码,将结构体自身类型再定义到本结构体当中做法是不可取的,程序在编译时就会报错,原因是这样会使结构体的声明无限的嵌套下去,结构体的大小也无法判断。
2.正确示范:
struct S1 { int data; struct S1* next;//结构体指针类型的next };
在向结构体中包含一个结构体变量时,声明的结构体内的结构体类型必须是结构体指针,这个指针指向的是一个结构体类型,通过这个指针就可以找到一个结构体类型。
3.结构体的重命名:
错误代码:
typedef struct S2 { int data; S2* next; }S2;
这里在结构体声明时使用了结构体重命名后的名字,由于程序在执行时是从上到下进行的,所以在使用重命名的结构体时会发生异常。
正确示范:
typedef struct S2 { int data; struct S2* next; }S2;
在定义这个结构体时还是要使用重命名前的结构体名才能成功。
🍁1.3结构体定义和初始化🍁
结构体类型声明后,我们该如何定义结构体变量呢,通过下面的代码结合注释可以清晰地理解在不同位置创建变量后的不同效果。
全局变量:
#include <stdio.h> struct S { int x; int y; }s1;//声明结构体时定义结构体变量,这里的s1是全局的结构体变量 struct S s2;//全局结构体变量的定义 struct S s3 = { 1,2 };//全局结构体变量的定义同时赋值 int main() { printf("%d %d\n", s3.x, s3.y); return 0; }
局部变量:
#include <stdio.h> struct S { int x; int y; }; int main() { struct S s1;//局部结构体变量的定义 struct S s2 = {1,2};//局部结构体变量的定义及赋值 printf("%d %d\n", s2.x, s2.y); return 0; }
🍁1.4结构体的内存对齐🍁
基本了解完结构体后,如何计算结构体的大小呢,是决定于结构体内的全部成员变量的大小、还是所有成员中占最大空间的成员的大小呢,看完接下来的结构体的内存对齐你就会知道答案。
🍄1.4.1对齐规则🍄
要想知道结构体的大小,就要了解结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处
2. 其他成员变量要对齐到该成员对齐数的整数倍的地址处
------对齐数 = 编译器默认的一个对齐数与该成员大小的较小值)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
只看这些文字可能还是不太清楚,我们计算几道类型题,通过练习这几道题目就能基本掌握这个对齐规则。
subject_1:
#include <stdio.h> struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct S1));//输出为12 return 0; }
c1是char类型,占1个字节,VS下默认对齐数是8,取较小值,c1的对齐数就是1,i是int类型,占4个字节,i的对齐数就是4,同理,c2的对齐数是1,c1从0偏移处开始占1个字节,由于i的对齐数是4,下一次偏移量为4的倍数处的地址开始占用空间,所以1、2、3偏移处的地址所在的空间会被浪费掉,i从4的偏移地址处开始占用空间,c2对齐数是1,直接往后占用1个字节空间,此时到达9偏移处,三个成员中最大对齐数是4,最后结构体的大小必须是4的倍数,所以还需往后浪费4个字节空间到达12偏移处,结构体大小就是12个字节。
subject_2:
#include <stdio.h> struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S2));//输出为8 return 0; }
c1是char类型,占1个字节,对齐数是1,c2也是char类型,对齐数是1,i是int类型,占4个字节,与默认对齐数相比较小,所以对齐数是4,c1从0偏移处地址开始往后占1个字节空间大小,c2继续往后占1个字节空间大小,此时偏移量是1,而i的对齐数是4,需要浪费两个字节空间到偏移量为4的地址处,往后再占4个字节空间,此时偏移量为7,三个成员中最大对齐数是4,最后结构体的大小必须是4的倍数,需要再浪费掉1个字节空间,最后结构体大小是8个字节。
subject_3:
#include <stdio.h> struct S3 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct S3));//输出为16 return 0; }
d是double类型的,占8个字节,对齐数就是8,c对齐数是1,i对齐数是4,d从0偏移处开始往后占8个字节的空间,到达7偏移处,c直接往后占1个字节空间,到达8偏移处,浪费了3个字节空间到达12偏移处是i的对齐数4的倍数,往后占4个字节空间到达15偏移处,三个成员中最大对齐数是8,最后结构体的大小必须是8的倍数,需要再浪费掉1个字节空间,最后结构体大小是16个字节。
subject_4:
struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; int main() { printf("%d\n", sizeof(struct S4));//输出为32 return 0; }
c1是char 类型的,占1个字节,对齐数是1,s3是struct S3结构体类型,s3的成员中最大对齐数是8,所以s3的对齐数必须是8的倍数,而s3的大小是16,默认对齐数是8,所以s3的对齐数就是8,d是double类型的,占8个字节,对齐数就是8,c1从0偏移处占1个字节,浪费掉7个字节空间到达8偏移处的地址,往后数16个字节的空间到达23偏移处地址,d再往后占8个字节空间到达31偏移处地址,三个成员中最大对齐数是8,最后结构体的大小必须是8的倍数,需要再浪费掉1个字节空间,最后结构体大小是32个字节。
🍄1.4.2修改默认对齐数🍄
结构体有了对齐规则,它的成员在内存中的存储也就有了更节省空间的可能,但是在不同编译器下默认对齐数也有所差异,但是我们可以在对齐方式不合适的时候,自己更改默认对齐数。
修改默认对齐数:
#include <stdio.h> #pragma pack(8)//设置默认对齐数为8 struct S1 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 #pragma pack(1)//设置默认对齐数为1 struct S2 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 int main() { printf("%d\n", sizeof(struct S1));//6 printf("%d\n", sizeof(struct S2));//12 return 0; }
🍁1.5结构体传参🍁
结构体传递参数有两种方式
1.传结构体,2.传结构体地址
但最好的方式是传结构体地址,因为函数在传参的时候,参数需要压栈,如果结构体过大,在函数压栈时会消耗更长的时间,且函数对结构体的操作结果一般是带不到主调函数中的,导致性能下降,所以结构体传参时最好传递结构体指针。
#include <stdio.h> 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; }
🥭二、位段🥭
位段和结构体类似,不同的是,位段的成员必须是int、unsigned int 或signed int 、char,位段的成员名后边有一个冒号和一个数字。
一个位段:
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
🍁1.位段的内存分配🍁
位段的大小是如何计算的呢?
struct A { int _a:2; int _b:5; int _c:10; int _d:30; }; int main() { printf("%d\n", sizeof(struct A));//输出8 return 0; }
为什么会输出8呢,位段A中有4个int型的变量,为什么大小不是16呢,我们通过下图来理解:
位段的位指的是二进制位,冒号后的数字指的是该变量所占二进制位的大小。
🍁2.位段的跨平台问题🍁
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
🥭三、枚举🥭
🍁1.类型定义🍁
enum Color//颜色 { RED, GREEN, BLUE };
枚举就是一一列举,其内的成员都是可能取值,也叫枚举常量,都赋有初值,默认从0开始依次按1递增,再定义时也可以赋初值。
🍁2.枚举的优点🍁
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
枚举可作为类型调试中可以观察到:
#define定义的无法观察到类型:
🍁3.枚举的应用🍁
输入1-7的数字,输出对应星期:
#include <stdio.h> enum week { EXIT, MON,//1 TUES,//2 WED,//3 THUR,//4 FRI,//5 SAT,//6 SUN//7 }; int main() { int input; do { printf("请输入你的操作:>"); scanf("%d", &input); switch (input) { case MON: printf("星期一\n"); break; case TUES: printf("星期二\n"); break; case WED: printf("星期三\n"); break; case THUR: printf("星期四\n"); break; case FRI: printf("星期五\n"); break; case SAT: printf("星期六\n"); break; case SUN: printf("星期日\n"); break; case EXIT: printf("退出!\n"); break; default: printf("请重新输入!\n"); } } while (input); return 0; }
代码实现结果:
🥭四、联合体🥭
🍁1.联合体介绍🍁
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体),联合体在使用时不能同时使用所有成员变量。
//联合类型的声明 union Un { char c; int i; };
🍁2.联合体大小🍁
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员),当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include <stdio.h> union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; int main() { //下面输出的结果是什么? printf("%d\n", sizeof(union Un1));//8 printf("%d\n", sizeof(union Un2));//16 return 0; }
观察如下图理解:
用联合判断大小端:
#include <stdio.h> int check() { union { char c; int i;//c和i共用同一块空间 }un; un.i = 1;//i为1 -- 16进制:00 00 00 01 return un.c;//小端返回01,大端返回00 } int main() { int ret = check(); if (1 == ret) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
代码结果:
五、总结(通讯录的预实现)
了解完结构体之后,结合现有的的知识,我们可以完成一个简单的通讯录,通过结构体来实现通讯录中人的信息的记录,通讯录的具体的细节我都放在我的gitee仓库中了,有需要的友友可以点击自取,喜欢的伙伴千万别忘了一键三连(我的主页)🐶蟹蟹大家的支持!!!