🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🐌 个人主页:蜗牛牛啊
🔥 系列专栏:初出茅庐C语言
☀️ 学习格言:眼泪终究流不成海洋,人总要不断成长!
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹
文章目录
一、结构体
结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。
1.结构体的声明
structtag{
member - list;}variable - list;
struct
是结构体关键字,
tag
是结构体标签,
member - list;
是成员变量列表,
variable-list
是变量列表。结构体的成员可以标量、数组、指针甚至是其他结构体。
例如要用结构描述一个人的基本信息时,我们可以用结构体声明,如下:
structpopInfo{char name[20];//姓名char sex[2];//性别int age;//年龄char id[20];//身份证号} s1;
其中
popInfo
是结构体标签,
{ char name[20]; char sex[2]; int age; int id[20];
是结构体成员,要注意最后的
;
不能省略,
s1
是结构体变量。
但是我们在平时阅读代码的时候可能会看到这种代码:
//匿名结构体类型struct{char name[20];//姓名int age;//年龄char id[20];//学号};
这种声明省略了结构体的标签,是一种不完全的声明(匿名结构体类型),要想创建变量必须在结构体声明的时候直接定义并初始化。
下面这段代码只在结构体声明时定义了两个结构体全局变量
s1
和
s2
,并没有对其初始化,在VS2022下会提示不正确。
struct{char name[20];//姓名int age;//年龄char id[20];//学号}s1, s2;intmain(){//struct s3;//这样定义结构体变量是不可行的
s1 ={"zhangsan",20,"123456789"};//err,在VS2022下报错
s2 ={"lisi",19,"147258369"};//err,在VS2022下报错return0;}
在对匿名结构体类型进行定义时,还要对其进行初始化。例如下面这段代码:
#include<stdio.h>struct{char name[20];//姓名int age;//年龄char id[20];//学号}s1 ={"zhangsan",20,"123456789"}, s2 ={"lisi",19,"147258369"};intmain(){//struct s3;//这样定义结构体变量是不可行的//s1 = { "zhangsan" ,20, "123456789" };//err,会报错//s2 = { "lisi" ,19, "147258369" };//err,会报错printf("%s %d %s\n", s1.name, s1.age, s1.id);//输出结果为zhangsan 20 123456789printf("%s %d %s\n", s2.name, s2.age, s2.id);//输出结果为lisi 19 147258369return0;}
那么两个相同的匿名结构体,它们在声明时所定义的变量类型时相同的吗?答案是不相同。代码如下:
struct{char name[20];//姓名int age;//年龄char id[20];//学号}s;struct{char name[20];//姓名int age;//年龄char id[20];//学号}*p;intmain(){
p =&s;return0;}
运行代码后,会有如下警告:
编译器会把上面的两个声明当成完全不同的类型,所以是非法的。
2.结构体变量的定义和初始化
在定义结构体变量的同时给结构体变量赋值叫做结构体变量的初始化。
#include<stdio.h>structpopInfo{char name[20];//姓名char sex[5];//性别int age;//年龄char id[20];//身份证号} s1;//声明类型的同时定义变量s1(定义时没有在函数内时s1是全局变量)。structpopInfo p1;//定义结构体变量p1,p1是全局变量intmain(){structpopInfo s2;//一般在main函数中定义,s2是局部变量structpopInfo s3 ={"zhangsan","nan",20,"123546879"};//初始化,s3是局部变量printf("%s %s %d %s\n", s3.name, s3.sex, s3.age, s3.id);//输出结果为zhangsan nan 20 123546879
s3.age =30;//修改结构体变量s3中的变量值printf("%s %s %d %s\n", s3.name, s3.sex, s3.age, s3.id);//输出结果为zhangsan nan 30 123546879//s3.name = "lisi";//err,s3.name拿到的是name数组中首元素的地址,给地址赋值不可行//*(s3.name) = "lisi";//得不到想要的效果,*(s3.name)得到的是数组name中的首字符,给首字符赋值是不正确的//scanf("%s", s3.name);//改变name数组中的值strcpy(s3.name,"lisi");//将字符串"lisi"拷贝给name数组,改变了name数组中的值printf("%s %s %d %s\n", s3.name, s3.sex, s3.age, s3.id);return0;}
3.结构体的嵌套
结构体里面包含另一个结构体,叫做结构体的嵌套。
#include<stdio.h>structS{int x;int y;};structP{int x;structS s1;char ch;};intmain(){structP p1 ={5,{10,15},'z'};printf("%d %d %d %c\n", p1.x, p1.s1.x, p1.s1.y, p1.ch);//输出结果为 5 10 15 z//嵌套的话先找结构体变量再找结构体成员return0;}
4.结构体的自引用
结构体的自引用:在结构中包含一个类型为该结构本身的成员。
如果用下面的代码声明结构体不能得出结构体的大小,
sizeof(struct Node)
无法计算。
structNode{int data;structNode next;};
那我们可以利用一个struct Node类型的指针记录下一个结构体的地址,这样便能实现结构体的自引用。一个struct Node指针的大小也就是4/8个字节,通过这样的方式能够计算出结构体的大小。
struct Node* next
中
next
是指针,指向的类型是
struct Node
。
我们还可以利用
typedef
关键字对结构体进行类型重命名,Node此时就是struct Node类型。结构体声明中不能直接利用
Node* next
。
structNode{int data;structNode* next;};typedefstructNode{int data;structNode* next;}Node;
5.结构体内存对齐
在学习结构体内存对齐之前我们先学习一个函数 offsetof,它的头文件是
stddef.h
。
offset
的两个参数,
type
是结构体类型,
member
是结构体成员名,
offset
作用是计算结构体成员相对于起始位置的偏移量。
#include<stdio.h>#include<stddef.h>structS{char c1;int i;char c2;};intmain(){printf("%d\n",offsetof(structS, c1));//0printf("%d\n",offsetof(structS, i));//4printf("%d\n",offsetof(structS, c2));//8printf("%d\n",sizeof(structS));//12return0;}
通过上面的代码我们可以知道
struct S
中
c1
相对于起始位置偏移量是0,
i
相对于起始位置偏移量是4,
c2
相对于起始位置的偏移量是8。通过下面一张图加深理解:
通过上面的代码我们知道结构体的大小是12个字节,但是图中总共利用的空间也就只有9个字节,但是为什么结构体的大小是12个字节呢?这里我们就要引入结构体的内存对齐规则。
结构体的内存对齐规则:
(1)结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处。
(2)从第二个成员开始,要对齐到某个对齐数的整数倍的偏移处。(对齐数:结构体成员自身大小和默认对齐数中的较小值,VS下默认对齐数是8个字节;Linux环境下默认不设对齐数,对齐数是结构体成员的自身大小)
(3)结构体的总大小必须是最大对齐数的整数倍。
(4)如果嵌套了结构体的情况,嵌套的结构体对齐到最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)中最大对齐数的整数倍。
那么通过结构体内存对齐规则,我们分析一下下面的代码:
structS{char c1;int i;char c2;};
在
struct S
中,
c1
本身是
char
类型的,占1个字节,VS环境下默认对齐数是8,根据对齐数的定义可以知道
c1
的对齐数是1和8中的最小值,那么
c1
的对齐数就是1;
i
是
char
类型的,它自身大小是4,VS环境下默认对齐数是8,那么
i
的对齐数就是4;
c2
是
char
类型的,它自身大小是1,VS环境下默认对齐数是8,那么
c2
的对齐数就是1。知道了他们的对齐数,那么他们在内存中是怎样存放的呢?我们就来利用结构体的内存对齐规则来解释一下
struct S
中的成员变量是如何存放的。
根据结由结构体内存对齐规则我们知道结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处,所以
c1
存放在偏移量为0的大小为1个字节的空间,结构体中从第二个成员开始,要对齐到某个对齐数的整数倍的偏移处,第二个成员是
i
,他的对齐数是4,所以要从4的整数倍的偏移处那里开始存放,所以存放在偏移量为4的那个位置,
c2
的对齐数是1,存放在
i
的后面,结构体的总大小必须是最大对齐数的整数倍,在
struct S
中结构体成员的最大对齐数是4,所以结构体的大小是4的整数倍,结构体的大小就是12。
那我们再来计算一下下面结构体的大小。
structS1{char c1;char c2;int i;};
c1
的对齐数是1,
c2
的对齐数是1,
i
的对齐数是4,那在空间中分配内存时应该是这样的:
通过上面内存分配图我们可以发现
struct S
和
struct S1
两种类型的结构体成员类型相同,但是结构体所需空间却有很大差异,那么我们可以发现在设计结构体的时候,让占用空间小的成员尽量集中在一起,既能满足空间对齐,又能节省空间。
我们通过下面的代码分析一下在嵌套结构体中内存对齐规则的应用:
structS2{double d;char ch;int i;};structS3{char c;structS2 s2;double d;};
分析:
struct S2
中
d
的自身大小是8个字节,VS环境下默认对齐数是8,
ch
的对齐数是1,
i
的对齐数是4,在
struct S3
中
c
的对齐数是1,
struct S2
是嵌套的结构体,嵌套的结构体对齐到最大对齐数的整数倍处,
struct S2 s2
中的最大对齐数是8,所以在分配空间时要对齐到8的整数倍处,
struct S3
中
d
的对齐数是8。
在内存中的空间分配如下:
扩展:结构体在对齐方式不合适的时候,我们可以修改默认对齐数。
设置默认对齐数:
pragma pack(n)
,n是你想要修改的数值,只能是整数。
#pragmapack(n)//设置默认对齐数
恢复默认对齐数:
pragma pack()
#pragmapack()//恢复默认对齐数
为什么会存在内存对齐?可以归结为两点:
(1)平台原因(移植原因):不是所有的硬件平台都能访问地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐到内存访问只需要一次访问。
总的来说,结构体的内存对齐就是拿空间换时间的做法。
二、结构体成员的访问和结构体传参
结构体传参可以分为传值调用和传址调用,传值调用的时候函数的参数可以用结构体类型的变量来接收;传址调用的时候函数的参数可以用结构体指针变量来接收。
结构体成员的访问可以有两种方式:结构体变量.结构体成员名;结构体指针->结构体成员名。
我们通过下面的实例来演示:
//打印结构体中的数据#include<stdio.h>structS{int data[10];char buf[10];};voidPrint1(structS s){int i =0;for(i =0; i <10; i++){printf("%d ", s.data[i]);}printf("\n");printf("%s\n", s.buf);}voidPrint2(structS* ps){int i =0;for(i =0; i <10; i++){//printf("%d ", (*ps).data[i]);printf("%d ", ps->data[i]);}printf("\n");//printf("%s\n", (*ps).buf);printf("%s\n", ps->buf);}intmain(){structS s1 ={{666,888,999},"abc"};Print1(s1);//传值调用,将s1中的值传给sPrint2(&s1);//传址调用,将s1的地址传给psreturn0;}
当结构体传参的时候我们首选传址调用,因为传值调用参数压栈系统开销大,会造成系统性能下降。
三、位段
1.位段的声明
位段的声明和结构体是类似的,有两个不同:
(1)位段的成员必须是
int
、
unsigned int
和
signed int
(
char
也可以)
(2)位段的成员名后边有一个冒号和一个数字。后面的数字其实所占的二进制位数。
通过代码来说明位段的声明:
#include<stdio.h>structA{int _a :2;int _b :5;int _c :10;int _d :30;};intmain(){printf("%d",sizeof(structA));//输出结果为8return0;}
A
就是一个位段类型—
sizeof(struct A)
大小是8个字节。
位段其中的位其实是二进制位,上面代码的意思是:
_a
占了2个比特(bit)位,
_b
占了5个比特(bit)位,
_c
占了10个比特(bit)位,
_d
占了30个比特(bit)位。
2.位段的内存分配
(1)位段的成员必须是
int
、
unsigned int
、
signed int
和
char
(
char
是整型家族,char和int一般不会混着用)。
(2)位段的空间上是按照需要以4个字节(
int
)或者1个字节(
char
)的方式来开辟的。
(3)位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
现在我们来分析这段代码:
structA{int _a :2;int _b :5;int _c :10;int _d :30;};
刚开始开辟4个字节的空间(4Byte=32bit),给
_a
分配2个比特位,还剩30bit,再给
_b
分配5个比特位bit,还剩25个比特位,给
_c
分配10个比特位之后,还剩15个,不能达到
_d
所需要的空间,再开辟4个字节的空间,然后给
_d
30个比特位。所以此时
struct A
占用了8个字节的空间。
我们通过练习的方式加深对位段内存分配的理解,看下面代码:
#include<stdio.h>structS{char a :3;char b :4;char c :5;char d :4;};intmain(){structS s ={0};
s.a =10;
s.b =12;
s.c =3;
s.d =4;return0;}
解析:因为是
char
类型的,刚开始开辟1个字节,给
s.a
分配3个比特位,但是
s.a
用二进制表示为1010,位段的存放形式是不确定的,假设是从低位到高位存储的,那
s.a
放进去的是010;
s.b
用二进制表示为1100,那放进去的就是1100;
s.c
用二进制表示为11,刚才的那个字节只剩下1个比特位,需要再开辟1个字节的空间,放进去之后不够5个比特位,高位补0,放进去就是00011;
s.d
用二进制表示为100,需要4个比特位,刚才开辟只剩下3个比特位,还需要再开辟1个字节用来存放
s.d
,100不够4个比特位,高位补0,那就是0100。分配的内存可见下图:
在VS2022(X86)环境下可以看到内存中的数据:
扩展:位段的跨平台问题:
(1)
int
被当成有符号数还是无符号数是不确定的。
(2)位段中最大位数的的数目不能确定(16位机器最大16,32位机器最大32,如果写成27,在16位机器上会出问题。)
(3)位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。
(4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台问题的存在。
四、枚举
枚举顾名思义就是把可能的取值都列举出来。
enumDay{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
上面的代码中,
enum
枚举关键字,
Day
是枚举标签,
{ }
中是枚举可能的取值。
通过下面的代码去理解枚举:
#include<stdio.h>intmain(){enumColor{//枚举里面的每一项都是可能取值,每一个可能的取值都是常量
red,//刚开始的时候是常量0,每次增长1
black,
green=5,//可以在定义的时候更改常量值,后面不能修改
blue
};printf("%d %d %d\n", red, green, blue);enumColor c = red;//red = 5;//err,不能在这里修改enum里的常量值printf("%d\n", c);return0;}
解析:输出结果为 0 5 6 0。因为枚举中的第一个可能取值默认是0,可以在设定枚举的时候改变枚举的常量值,
red
的值是0,
black
的值是1,因为有
green=5
,就将
green
的值改为5,如果不改他的值是2,
green
的值改变,他后面的常量值也会改变,
blue
的值为6。
enum Color c = red;
,将
red
的值赋给
c
,所以
c
的值是0。
枚举类型的大小是4个字节,因为里面的值都是可能取值。
我们可以使用
#define
定义常量,为什么要使用枚举?因为枚举类型有如下优点:
(1)增加代码的可读性和维护性
(2)和
#define
定义的标识符比较枚举有类型检查,更加严谨。
(3)防止了命名污染(封装)
(4)便于调试
在这里插入代码片
(5)使用方便,一次可以定义多个常量。
比如下面的代码中用
#define
定义标识符,在预处理之后,代码中的red都被替换成5,
#define
已经不在了。
#definered5int num=red;printf("%d",num);
五、联合体
联合体类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间,所以也叫共用体。联合体也是一种特殊的自定义类型。
联合体的大小:联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union um {int i;char q[5];};
上面的代码中最大对齐数是4,
char q[5]
所占用的空间是5个字节,对齐数的大小看的是数据类型,所以他的对齐数是1,要保证联合体的大小是最大对齐数的整数倍,也就是4个整数倍,就是8。
union um {int i;short s[5];};
这段代码中的
i
的对齐数是4,
short s[5]
所占的空间大小是10个字节,
short
类型的大小是2个字节,所以
short s[5]
的对齐数是2,要保证联合体的大小是最大对齐数的整数倍,也就是4个整数倍,就是12。
我们可以通过下面的代码了解一下联合体是如何共用一块空间的。
#include<stdio.h>union um {char ch;int i;double d;};intmain(){union um un;printf("%d\n",sizeof(union um));printf("%p\n",&(un.ch));printf("%p\n",&(un.i));printf("%p\n",&(un.d));return0;}
解析:输出结果为8 003FFE28 003FFE28 003FFE28。因为联合体共用一块空间,所以他们的地址相同,并且会被覆盖。在这个联合体中最大成员的大小就是最大对齐数的整数倍,就是8。
联合体的运用
我们之前是通过强制类型转换判断大小端的,代码如下:
#include<stdio.h>intmain(){int num =1;char* ch =(char*)(&num);if(*ch ==1)printf("小端\n");elseprintf("大端\n");return0;}
我们还可以利用联合体巧妙的解决如何判断大小端问题。
#include<stdio.h>union um {int i;char q;};intmain(){union um un;
un.i =1;if(un.q ==1)printf("小端\n");elseprintf("大端\n");return0;}
利用联合体共用一块空间的性质,先将
i
赋值1之后,再通过
char
类型的变量一个字节一个字节取出来进行判断,如果是小端存储,
un.q==1
就成立,反之,则不成立。
版权归原作者 蜗牛牛啊 所有, 如有侵权,请联系我们删除。