文章目录
前言为什么要学习结构体什么是结构体一、结构体:struct1、结构体类型的声明(1)结构体的基础知识(2)结构体的声明(3)特殊的声明2、结构体的自引用3、结构体变量的定义和初始化(1)单一结构体的定义和初始化(2)嵌套结构体的定义和初始化4、结构体内存对齐(1)单一结构体内存对齐(2)嵌套结构体内存对齐(3)对齐规则(4)为什么存在内存对齐(5)如何利用内存对齐(6)修改默认对齐数(7)offsetof()函数5、结构体传参6、结构体实现位段(位段的填充&可移植性)(1)什么是位段(2)位段的内存分配(3)位段的跨平台问题(4)比较二、枚举:enum1、枚举类型的定义2、枚举的优点3、枚举的使用三、联合体(共用体):union1、联合体类型的定义2、联合体的特点3、联合体大小的计算练习1练习2总结前言
关于C语言,在指针学习过程中,在学习之余,做了一次总结,写成了一篇详解博客,也向大家分享的我对于指针的见解,我发现将所学的内容写成博客,不仅仅可以使和我一样的初学者们更快的了解相关知识点,还可以让我查缺补漏,弥补自己的短板,让我的基础更加扎实,所以在学完结构体以后,我也将结构体的内容整理成一篇博客,向大家分享。
为什么要学习结构体
当我们需要表达一个数据的时候,我们就需要用到变量,而变量又需要定义一个类型。我们通过之前的学习,知道了C语言中变量类型有:int、double、char、float等等基础类型,还有指针等等。但是如果我们想表达的数据比较复杂,不是一个数据,例如:日期(年、月、日)、学生信息(姓名、性别、年龄等等)、时间(时、分、秒)等等。而我们又想用一个整体来表达这些所有的数据,这个时候我们就需要用到一个自定义变量类型——结构体。
什么是结构体
结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员(或称为域,或称为元素)的不同数据组成,其中每个成员可以具有不同的类型。结构体通常用来表示类型不同但是又相关的若干数据。
注意:结构体是一种数据类型!!!
一、结构体:struct
1、结构体类型的声明
(1)结构体的基础知识
成员变量:结构是一些值的集合,这些值称为成员变量。
结构体的每个成员可以是不同类型的变量。
(2)结构体的声明
结构体的定义如下所示,struct为结构体关键字,tag为结构体的标志,member-list为结构体成员列表,其必须列出其所有成员;variable-list为此结构体声明的变量。
struct tag {member-list}variable-list;
例1:描述一个学生信息
#include<stdio.h>//声明一个结构体类型//声明一个学生类型,是想通过学生类型来创建学生变量(对象)//描述学生:属性·名字+电话+性别+年龄struct Stu{char name[20];//名字char tele[12];//电话char sex[5];//性别int age;//年龄}s4,s5,s6;//分号不能丢struct Stu s3;//s3,s4,s5,s6为全局变量int main(){//创建结构体变量struct Stu s1;struct Stu s2;//s1,s2为局部变量return 0;}
易错提示: 一定不要忘记结束时的分号!!!
(3)特殊的声明
在声明结构的时候,可以不完全的声明
例2:匿名结构体类型
#include<stdio.h>struct{int a;char b;float c;}x;struct{int a;char b;float c;}* p;int main(){p = &x;//错误 E0513不能将 "struct <unnamed> *" 类型的值分配到 "struct <unnamed> *" 类型的实体return 0;}
上面的两个结构在声明的时候省略了结构体的标志(tag)。(是正确的)
上述代码的问题出现在:p = &x。//这是非法的。
上面的两个匿名结构体虽然各自的成员是一模一样的,但是在编译器看来,它们是两个不同的类型,所以出现了报错(部分编译器是警告)。
2、结构体的自引用
结构体的自引用就是指在结构体内部,包含指向自身类型结构体的指针。
在之前的学习中,我们知道了在函数中可以包含自己(即递归),那么在结构中包含一个类型为该结构体本身的成员是否可以呢?
例3:
#include<stdio.h>struct Node{int data;//4struct Node n;//4+套娃};int main(){sizeof(struct Node);return 0;}
运行结果:
错误 C2460 “Node::n”: 使用正在定义的“Node”
这是为什么呢?因为n定义中又有n,无限循环,系统无法确定该结构体的长度,会判定定义非法。
切记,结构体自引用,成员定义只能是指针。
例4:正确的自引用
#include<stdio.h>struct Node{int data;//4struct Node* n;//4/8};int main(){int a = sizeof(struct Node);printf("%d", a);return 0;}
运行结果为:8(因为博主是32位)
这是为什么呢?因为我们在自引用时把结构的成员定义为指针,又指针的长度是确定的(上一节指针详解中提到过),所以此时结构体的长度也是确定的。
这时候可能有人又有疑问了,我们刚刚学了一种特殊的声明方式——匿名,那在自引用的时候,我们可不可以使用匿名了,就拿例4来举例子,因为这时候,我们使用了匿名结构,所以里边使用“Node* n;”不就好了吗?我们来看看,结构是怎样的:
例5:
#include<stdio.h>struct{int data;Node* n;}Node;int main(){int a = sizeof(struct Node);printf("%d", a);return 0;}
运行结果为:一大堆报错
这是为什么呢?因为我们在声明结构体的内部,就使用了Node这个变量,但是我们的编译器是在声明结构体结束以后,才接收到Node这个变量,所以,在使用Node变量的时候,编译器无法识别,就自然会出现错误。
建议:在使用结构体自引用的时候最好不要使用匿名声明结构体。
3、结构体变量的定义和初始化
前面我们了解了如何声明结构体的类型,现在我们有了结构体类型,那么我们要如何定义一个结构体变量以及初始化一个结构体变量呢?其实非常简单。
(1)单一结构体的定义和初始化
例6:
#include<stdio.h>struct Stu{char name[20];int age;char sex[5];}s1;int main(){struct Stu s2 = {"lisi",18,"nan" };printf("%s %d %s", s2.name[20], s2.age, s2.sex[5]);return 0;}
运行结果为:
lisi 18 nan
PS:这里博主用的是Visual Studio
通过struct+结构体的标志(tag)+变量名,就完成了结构体的定义;而在{}内把结构体成员对应的类型用逗号隔开赋值给声明的结构体,我们就完成的结构体的初始化。
(2)嵌套结构体的定义和初始化
刚刚我们了解了结构体的自引用,了解了结构体内是可以存在结构体的,也就是结构体的嵌套,现在我们了解了单一的结构体如何定义和初始化,那有人就会想了,嵌套结构体如何进行定义和初始化呢?
例7:
#include<stdio.h>struct T{int c;double weight;};struct Stu{char name[20];struct T p;int age;char sex[5];};int main(){struct Stu s = {"lisi",{30,1.0},18,"nan"};printf("%d %lf",s.p.c,s.p.weight);return 0;}
运行结果为:30 1.000000
在结构体中遇到结构体,我们在初始化的时候,同样的方法在外层结构体的{}内再添加一个{}即可。
注意:嵌套结构体在调用的时候,逐层调用。
4、结构体内存对齐
通过前面的学习,我们已经掌握了结构体的基本使用了。
有人就又会问了,结构体是变量,那变量就有大小啊,我们如何计算结构体的大小呢?
这里就涉及到了一个热门的考点:结构体内存对齐。
(1)单一结构体内存对齐
先来做一道练习题:
例8:
#include<stdio.h>struct s1{char c1;int a;char c2;};struct s2{char c1;char c2;int a;};int main(){struct s1 s1 = {0 };printf("%d\n", sizeof(s1));struct s2 s2 = {0 };printf("%d\n", sizeof(s2));return 0;}
运行结果为:
12
8
大家第一次拿到这个题,肯定会想:这有什么好算的,不就是6、6吗?但是结构体的大小计算不是这样随便计算的,它需要符合一定的条件。
那么到底如何计算呢?我们需要利用结构体对齐规则:
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数= 编译器默认的一个对齐数 与 该成员大小的较小值
提示:VS中默认的值为8
③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
看完对齐规则,我们回到例8
㈠先看到struct s1这个结构体
第①步 结构体存放变量从偏移量为0的位置开始:
就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
a的对齐数为4,所以a要放到4的整数倍的地址处,所以a从4开始,又因为a是4个字节,所以a就是4 ~ 8;
到这里有人就会问了:那中间的1 ~ 4怎么办,中间这部分就浪费掉了。
c2的对齐数为1,c2是1个字节,所以从8开始,c2是8~9。
第③步 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
对于这个结构体中的成员:
c1的对齐数为1,a的对齐数为4,c2的对齐数为1,那么最大对齐数就是4;
而现在我们的一共用了9个字节,9不是4的整数倍,所以我们还要再浪费3个字节,达到4的整数倍12个字节。
㈡再看到struct s2这个结构体
第①步 结构体存放变量从偏移量为0的位置开始:
就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
c2的对齐数为1,所以c2要放到1的整数倍的地址处,所以c2从1开始,又因为c2是1个字节,所以c2就是1 ~ 2;
a的对齐数为4,所以a要放到4的整数倍的地址处,所以a从4开始,又因为a是4个字节,所以a就是4 ~ 8。
第③步 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
对于这个结构体中的成员:
c1的对齐数为1,c2的对齐数为1,a的对齐数为4,那么最大对齐数就是4;
现在我们一共用了8个字节,8是4的整数倍,所以这个结构体的大小就是8个字节。
趁热打铁,再来一道练习题:
例9:
#include<stdio.h>struct s3{double a;char b;int c;};int main(){printf("%d\n", sizeof(struct s3));return 0;}
运行结果为:16
你做对了吗?如果没做对,没关系重新来过,再温习一遍例题;如果做对了,是不是成就感满满,但是别急,下面还有更难的!
(2)嵌套结构体内存对齐
在思考每一个问题的同时,不要忘记我们学过的结构体是可以嵌套的。但是不要担心,我们的对齐规则考虑到了这种情况:
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
来看一道例题:
例10:
#include<stdio.h>struct s3{double a;char b;int c;};struct s4{char c1;struct s3 s3;double d;};int main(){printf("%d\n", sizeof(struct s4));return 0;}
运行结果为:32
还是按照步骤来解题:
第①步 结构体存放变量从偏移量为0的位置开始:
就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
对于struct s3这个结构体:
a的对齐数为8,b的对齐数为1,c的对齐数为4,所以最大对齐数为8;
所以s3要放到8的整数倍的地址处,所以s3从8开始,又因为s3是16个字节(例9),所以s3就是8~24;
d的对齐数为8,所以a要放到8的整数倍的地址处,所以a从24开始,又因为a是8个字节,所以a就是24 ~ 32。
第③步 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对于这个结构体中的成员:
c1的对齐数为1,s3的对齐数为8,d的对齐数为8,那么最大对齐数就是8;
现在我们一共用了32个字节,32是4的整数倍,所以这个结构体的大小就是32个字节。
(3)对齐规则
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数= 编译器默认的一个对齐数 与 该成员大小的较小值
提示:VS中默认的值为8
③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(4)为什么存在内存对齐
大家在对于对齐规则的学习中,肯定会有这样的疑问:
我们在对齐的过程中,浪费了那么多空间,那为什么还要存在内存对齐呢?
①平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
即 结构体的内存对齐是拿空间来换取时间的做法。
(5)如何利用内存对齐
前面我们了解到:内存对齐是拿空间来换取时间的做法。
那么我们如何做到既要满足内存对齐,又要节省空间呢?
让占用空间小的成员尽量集中在一起。
举个例子:
例11:
struct s1{char c1;int a;char c2;};struct s2{char c1;char c2;int a;};
这里s1和s2类型的成员是一模一样的,但是s2占用的空间比s1小。
(6)修改默认对齐数
在C语言中默认对齐数是可以修改的,利用 #pragma 这个预处理命令,就可以改变默认对齐数。
举一个例子:
例12:
#include<stdio.h>struct s1{char c1;double a;};#pragma pack(4)//设置默认对齐数为4struct s2{char c1;double a;};#pragma pack()//取消设置的默认对齐数int main(){printf("%d\n", sizeof(struct s1));printf("%d\n", sizeof(struct s2));return 0;}
运行结果为:
16
12
这里可以看到:
s1中,存放a的时候对齐数为8,所以a从8开始,又因为a是8个字节,所以a是8~16,所以s2是16个字节;
我们将默认对齐数修改为4的时候,s2中,存放a的时候对齐数为4,所以a从4开始,又因为a是8个字节,所以a是4~12,所以s2是12个字节。
(7)offsetof()函数
offsetof()函数是用来返回结构体成员的偏移量。
使用offsetof()函数时,需要加上 #include<stddef.h> 这个头文件
offsetof(variable-list,member-list)
举个例子:
例13:
#include<stdio.h>#include<stddef.h>struct s{char c;int a;double b;};int main(){printf("%d\n", offsetof(struct s, c));printf("%d\n", offsetof(struct s, a));printf("%d\n", offsetof(struct s, b));return 0;}
运行结果为:
0
4
8
5、结构体传参
直接上例子:
例14:
#include<stdio.h>struct s{char c;int a;double b;};void func1(struct s p){p.a = 100;p.b = 3.14;p.c = 'w';}void func2(struct s* p){p->a = 100;p->b = 3.14;p->c = 'w';}//传值void print1(struct s tmp){printf("%d %lf %c\n", tmp.a, tmp.b, tmp.c);}//传址void print2(struct s* tmp){printf("%d %lf %c\n", tmp->a, tmp->b, tmp->c);}int main(){struct s s = {0 };func1(s);print1(s);print2(&s);func2(&s);print1(s);print2(&s);return 0;}
运行结果为:
0 0.000000
0 0.000000
100 3.140000 w
100 3.140000 w
通过例14,我们可以看出func1进行传参,只是形参,func2进行传参,传的是地址;同样print1是传值,而print2是传址。两种传递方法都可以,但是我们更加提倡以地址的形式进行传递,因为这样是以指针的形式传递,无论结构体有多大,指针的大小均为4/8。
6、结构体实现位段(位段的填充&可移植性)
(1)什么是位段
位段的声明和结构是类似的,有两个不同:
①位段的成员必须是int、unsigned int 、 signed int 或 char。
②位段的成员名后边有一个冒号和一个数字。
来做一道题:
例15:
#include<stdio.h>//位段 - 二进制位struct s{int a : 2;//2个bitint b : 5;//5个bitint c : 10;//10个bitint d : 30;//30个bit};int main(){struct s s;printf("%d\n", sizeof(s));return 0;}
运行结果为:8
这里大家就会猜测说:2+5+10+30=47bit,那不应该是6个字节吗?为什么是8个字节啊,这是因为位段也有它的规则。
(2)位段的内存分配
①位段的成员可以是int、unsigned int 、 signed int 或 char(属于整形家族)类型。
②位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
③位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
现在我们再来看例15,:
a,b,c一共需要17个bit来存放,这时,需要开辟4个字节(32bit)的空间来存放;但是剩下的15个bit不足以存放d,所以就需要再开辟4个字节(32bit)的空间来存放d。(剩余的空间浪费了)
所以共需8个字节。
(3)位段的跨平台问题
①int位段被当成有符号数还是无符号数是不确定的;
②位段中最大位的数目不能确定;(16位机器最大16,32位机器最大32)
③位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义;
④当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
(4)比较
跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
二、枚举:enum
概念:一一列举,把可能的取值一一列举。
1、枚举类型的定义
举一个例子:
例16:一个人的性别
#include<stdio.h>//枚举类型enum Sex{//枚举的可能取值MALE,//男性FEMALE,//女性SECRET//秘密};int main(){enum Sex s = MALE;printf("%d %d %d\n", MALE, FEMALE, SECRET);return 0;}
运行结果为:0 1 2
注意:在定义枚举时,我们可以随意定义,但是如果没有赋值,会默认为0,1,2,……,同时枚举作为一个常量,我们无法在定义完成后进行修改。
2、枚举的优点
①增加代码的可读性和可维护性;
②和 #define 定义的标识符比较枚举,枚举具有类型检查,更加严谨;
③防止了命名污染(封装);
④便于调试;
⑤使用方便,一次可以定义多个常量。
3、枚举的使用
再举一个例子:
例17:
#include <stdio.h>enum DAY{MON = 1, TUE, WED, THU, FRI, SAT, SUN};int main(){enum DAY day;day = MON;printf("%d", day);return 0;}
运行结果为:1
三、联合体(共用体):union
1、联合体类型的定义
联合体也是一种特殊的自定义类型。这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
来看一个例子
例18:
#include<stdio.h>union un{char c;int i;};int main(){union un u;printf("%d\n", sizeof(u));printf("%p\n", &u);printf("%p\n", &(u.c));printf("%p\n", &(u.i));}
运行结果为:
4
004FFD3C
004FFD3C
004FFD3C
这也说明了:联合体的成员公用同一块空间。
2、联合体的特点
联合体的成员是公用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
3、联合体大小的计算
前面的结构体和枚举都有自己的规则,那联合体也不例外:
①联合体的大小至少是最大成员的大小;
②当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
练习1
判断当前计算机的大小端存储。
例19:
#include<stdio.h>int check_sys(){int a = 1;//返回1表示小端//返回0表示大端return *(char*)&a;}int main(){int a = 1;int ret = check_sys();if (1 == ret){printf("小端\n");}else{printf("大端\n");}//int a = Ox11223344;////低地址---------------------------------------------->高地址//...[][][][11][22][33][44][][][][]...大端字节序存储模式//...[][][][44][33][22][11][][][][]...小端字节序存储模式//讨论一个数据,放在内存中的存放的字节顺序//大小端字节序问题//return 0;}
运行结果为:小端
练习2
制作学生管理系统
这个博主现在正在研究,也欢迎大家来交流,因为目前能力所限,这个到时候会再写一篇博客,专门说明这个。
总结
不知不觉,结构体的内容已经结束了,博主从晚上七点奋战至凌晨四点,不得不感叹道:时间过的真快的。学习的时光总是美好的,每天能学到新的知识就会感到很充实,做这个博客的原因不光是想查缺补漏一下,更多的是想帮助那些初学者,让他们能够很快理解这些知识点,一起加油。
如果觉得《C语言自定义变量类型——结构体详解》对你有帮助,请点赞、收藏,并留下你的观点哦!