绪论
书接上回,通过上章的一些函数,我们可以让我们对于一些数值的调整有很大的帮助,本章自定义类型在C语言中同样也有着非常重要的地位,相信只要认真的阅读了本章,一定会对你有很大的帮助。
所以安全带系好,发车啦(建议电脑观看)。
思维导图:
要XMind思维导图的话可以私信哈
目录
1.结构体
1.1结构体的声明
1.2结构体变量的初始化
1.3结构体内存对齐
1.4修改默认对齐数
1.5结构体传参
2.位段
3.枚举(enum)
3.1枚举的定义
4.联合体(共用体union)
1.结构体
结构是指一些值的集合,这些值被称为成员变量,而结构体成员可以是不同的类型(类似数组,数组也是一些值的集合,不过这些数组的值的类型是统一的)。
而结构体就像一张张体检表,上面有着一个人的许多信息,或者某样物的许多信息的集合
1.1结构体的声明
知识点:
语法:
struct name
{
member;
.....;
};
对于里面的成员至少是1个;
下面直接通过代码的形式来展示如何声明(让这个结构体合法)一个结构体
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 };
这样就声明了一个结构体,其实结构体的声明也可以看成创建了一个新的类型,里面的成员有着不同的类型来修饰这个新的类型,这样就能对于一些复杂对象进行描述如上的学生
细节(注意点):
一种特别的声明匿名结构体类型(不完全声明):
struct { int a; };
此时的声明没有名字,只能跟在该声明后创建变量名/用typedef的实现
而对于结构体变量的定义有两种方法:
直接跟在结构体后面,这种是全局的
在函数内以结构体类型 + 变量名 的形式来创建一个变量(结构体类型是一种自定义类型),并且是局部的
具体如下:
//方法1 struct { int a; }s1,s2;//可以同时创建一个或多个结构体变量(s1,s2) struct Stu student = { "张三",20,"男"};//(全局的) //方法2 struct Stu { char name[20]; int age; char sex[5]; }; int main() { struct Stu student = { "张三",20,"男"};//(结构体变量student) return 0; }
所以对于匿名结构体来说他是没有结构体的名字的所以就会导致无法进行第二种,只能第一种直接在结构体后面创建变量,来使用和初始化。
用typedef来对结构体来进行简化重命名
typedef struct Stu { int age; char name[20]; char sex[5]; }Stu;
当typedef重命名匿名结构体时让匿名结构体就可以让其在其余地方用
typedef struct { int a; }a; int main() { a b = { 20 }; printf("%d", b.a); return 0; }
但是要注意当嵌套时不能省略里面
typedef struct Stu { int name; struct Stu * Node;//不能写成Stu* Node因为此时还不行因为重命名还在后面才执行 }Stu;
1.2结构体变量的初始化
知识点:
对于结构体变量的初始化的方法同样分为了两种方法,对应着创建结构体变量的两种方法:
在全局的结构体变量后直接初始化
struct Stu { char name[20]; int age; char sex[5]; }s1 = { "张三",20,"男"};
在局部的结构体变量后再初始化
struct Stu { char name[20]; int age; char sex[5]; struct Stu* a; }; int main() { struct Stu student1; struct Stu student = { "张三",18,"男",&student1 }; printf("%s %d %s %p", student.name, student.age, student.sex,student.a); return 0; }
单独逐一初始化
struct Stu { char name[20]; int age; char sex[5]; }student; int main() { student.age = 25; //字符串数组的话,你要想赋值,你就必须要通过拷贝 strcpy (student.name ,"LiSi"); strcpy (student.sex ,"男"); printf("%s %d %s",student.name,student.age,student.sex); return 0; }
细节:
不能在自己的结构体成员中有和自己一样的结构体类型这样的话这个类型的大小将无限大,(要创建只能是别的结构体类型)
struct Stu { int age; }; struct Stu { char name[20]; int age; //struct Stu b;error struct Stu p; char sex[5]; }s1 = { "张三",20,{0},"男"}; int main() { printf("%s %d %s", s1.name, s1.age, s1.p.age , s1.sex); return 0; }
当创建了结构体变量后,对于结构体变量初始化,只能单独改变了,不能同时改变了(只能在struct Stu student = {....}这种形式时改)
struct Stu { char name[20]; int age; char sex[5]; }; int main() { struct Stu student1 = { "张三",20,"男" }; student = { "lisi",18,"nan" };error student.age = 25; printf("%s %d %s", student.name, student.age, student.sex); return 0; }
乱序初始化:
struct Stu { char name[20]; int age; char sex[5]; struct Stu* a; }; int main() { struct Stu student1; struct Stu student = { .a = &student1 , .age = 20 , .name= "lisi",.sex= "nan"}; printf("%s %d %s %p", student.name, student.age, student.sex,student.a); return 0; }
1.3结构体内存对齐
知识点:
是当我们要求一个结构体大小时就需要用到结构体内存对齐
对齐规则:
第一个成员变量直接对齐到偏移量为0的地址处
从第二个成员变量开始要对齐到偏移量为自身对齐数的整数倍处 对齐数 = 判断自身大小和默认对齐数并取较小值(在vs环境下默认的一个对齐数值是8)
结构体的总大小最终要为所有成员变量中的取过的最大对齐数的整数倍
如果有镶嵌结构体,那这个最大对齐数的判断也要包括所镶嵌的结构体 内的 成员的对齐数 ,并且这个镶嵌结构体也要对齐到自身的最大的对齐数上(在外部的结构体内)
附:在Linux gcc 环境下没有默认对齐数 对齐数就是其本身大小
下面通过例子就可以更好的去理解和记忆
细节:
计算偏移量的宏offsetof
两个参数:offsetof(结构体类型,成员名)
头文件:#include<stddef.h>
为什么要有内存对齐
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(如int 4 double 8)
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(例:在32位机器上一次读取数据是4byte 假如结构体存中存着char 和 int 其中char 要占1byte int 占 4byte 这样总共就是5byte 那当我们要读取int 时机器就会先读取前面的4byte 那int还剩1byte要读所以就会再读一次,可若当我们进行了内存对齐就只需要一次就能将4byte的int 读完)
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
所以因为存在内存对齐:所以一个成员相同的结构体,其大小都有可能不同:
struct s { char c1; int i; char c2; };// 1 -> 4 + 4 -> 8 + 1 -> 12(要是最大对齐数的整数倍) struct c { char c1; char c2; int i; };// 1 -> 1 + 1 -> 4 + 4 -> 8
所以我们要有内存对齐的概念并且通过改变写法来节约空间
所以我们在写结构体时应该经量将小的成员放在一起
练习:
例1:
#include<stdio.h> #include<stddef.h> struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct S1)); printf("%d\n", offsetof(struct S1,c1)); printf("%d\n", offsetof(struct S1,i)); printf("%d\n", offsetof(struct S1, c2)); }
分析:
1.首先将c1对齐到偏移量为0处
2.其次看其余的成员变量:
i --> 自身大小为4byte 、 默认对齐数大小为8byte 、取较小值4为最终的对齐数(取较小值)
所以i要对齐到偏移量为4的倍数的地址去,0处往后就有偏移量为4处的地址可以 存i的4byte
c2 --> 自身大小为1byte、默认为8、取1为对齐数(取较小值)
c2要对齐到偏移量为1的倍数中去,4偏移处存了i的4byte后 就到了8偏移处 往后的任意位置都是1倍数,所以在8偏移处存c2
3.最后:从0 ~ 8 结构体的总大小变成了9并不是移处不是最大对齐数(1 4 1)4的倍数,所以最终要放到偏移量为11处,所以最终大小为12byte(0 ~ 11)
偏移量:可以看成距离起始位置(箭头指向的)的距离(看线)
附加一种我自己的判断方法(不用画图直接口算):
第一个成员直接放(毋庸置疑),从第二个成员开始就要满足从对齐数的整数倍地址放,这样举个例子来解释:如当一个int 类型大小为4 所以就要从偏移量(而这个偏移量就直接找倍数即可不用想的太多)为 4处开始 ,那么就有:
从某一开始的偏移量(满足对齐数的倍数) + 自身元素大小 = 总大小 / 所到的偏移量处(继续往后开始的偏移量)
这个公式是我自己总结的可以细品,多来两道体就可以很快的算出结构体大小
具体如下:
struct name { int a; char b; int c; int d; };
直接口算:
先放了int 这样就是 0 + 4 = 4 此时还没放完 所以是位移到了4偏移量处 (继续往后开始的偏移量)
再放char 此处因为char的对齐数肯定是1 所以可以放到任何位置 4 + 1 = 5
再放int 应该找到8偏移处开始, 所以就是 8 + 4 = 12 移动到12偏移处(继续往加开始的偏移量,直接找倍数"8")
再放int 直接从12开始 ,12 + 4 = 16 就为最终的总大小
再快一点就可以直接 :4 -> 4 + 1 -> 8(直接找倍数"8")+ 4 - > 12(已经是4的倍数) + 4 = 16
例2:
struct S3 { double d;// 对齐数8 char c;// 1 int i;//4 };//总大小为 8 -> 8 + 1 -> 12 + 4 -> 16 struct S4 { char c1;// 1 struct S3 s3;// 16 double d;// 8 }; //此处double类型的大小是8 默认也是8 所以最终对齐数就是8 ,其次注意嵌套结构体 放在外部的结构体时要对 //齐到嵌套结构体内部最大对齐数的整数倍即8的倍数 int main() { printf("%d\n", sizeof(struct S4));//总大小为:1 - > 8(直接找倍数) + 16 - > 24 + 8 -> 32 return 0; }
附:对于结构体成员是数组是如 char arr[5];这是其实可以直接把他们看成5和char类型的成员,char c1 ;char c2 ;char c3 ;char c4 ;char c5;在用对齐知识进行正常对齐
1.4修改默认对齐数
知识点:
如标题所示:默认对齐数是可以修改的方法如下:
#pragma pack( )在括号内填所要修改成的值:如#pragma pack( 4 )
如果#pragma pack( )单独出现时表示回复默认对齐数
细节:
不能随意的修改默认对齐数应该根据所需,如当修改成了#pragma pack( 1 )这样其实也就不存在内存对齐了
就会导致这时的对齐数一定都是1
所以当对齐方式不合适时,我们就可以自行修改默认对齐数
1.5结构体传参
知识点:
当我们把结构体变量放在函数里时,我们同样可以分成值传递、和址传递。
具体如下:
struct s { int a; char b; }s1; void print1(struct s s1) { printf("%d\n",s1.a); printf("%c\n",s1.b); } void print2(struct s * s1) { printf("%d\n",(*s1).a); printf("%c\n",(*s1).b); printf("%d\n",s1->a); printf("%c\n",s1->b); } int main() { print(s1); print(&s1); return 0; }
当然,print2传地址肯定比print1传值是要好的(函数传参要压栈,如果结构体过大对于时间和空间上都会有损失),所以最好直接统一直接传结构体的地址
练习:通讯录(学生信息管理系统)