🚀write in front🚀
📜所属专栏:c语言学习
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
文章目录
- 前言🤯
- 一:结构体🤩
- 1.1:结构的基础知识:
- 1.2:结构的声明:
- 1.3:特殊声明(匿名结构体):
- 1.4:结构的自引用:
- 1.5:结构体变量的定义和初始化:
- 1.5.1结构体变量的初始化:
- 1.5.2结构体嵌套结构体的初始化
- 1.6:<font color="red">结构体的内存对齐(超重点!!★★★):
- 1.6.1:结构体的数据在内存中如何存储的?
- 1.6.2:为什么存在内存对齐?
- 1.7:修改默认对齐数:
- 1.8:结构体传参:
- 二:位段
- 2.1:什么是位段?
- 2.2:位段的内存分配
- 2.3:验证vs2019上位段的内存分配和使用
- 2.4:位段的跨平台问题
- 2.5:位段的应用
- 总结:
前言🤯
今天分享的内容是自定义类型之一的结构体。C语言本身为我们提供了一些类型,比如 int、char、float等等,我们可以利用这些类型来定义一些比较简单的事物,那如果要定义一本书呢?C语言自身提供的这些类型,好像都无法精准的帮我们定义出一本书,一本书包含:书名、作者、出版社等主要信息。为此,C语言为我们提供了结构体这种自定义类型,我们可以根据自己的需求去定义结构体里的成员列表,用来描述不同类型行的事物。接下来就让我们一起来看看,结构体里都有哪些有趣的知识吧!
一:结构体🤩
1.1:结构的基础知识:
结构体是一些值的集合,这些值称为成员变量,结构的每一个成员可以是不同类型的变量
数组也是一些值的集合,但一个数组里面存的都是相同类型的元素
1.2:结构的声明:
struct tag//tag是标签
{
member-list;//成员列表,可以是一个或者多个
}variable-list;//变量列表
//结构体的声明:
struct student
{
char name[20];
int age;
char sex[5];
float score;
}s1,s2;
//定义结构体变量s1、s2
//此处定义的结构体变量是全局的
struct student s3, s4;
//定义结构体变量s3、s4
//此处定义的结构体变量等同于声明时定义,也是全局的
int main()
{
struct student s5, s6;
//定义结构体变量s5、s6
//此处定义的结构体变量是局部的
return 0;
}
1.3:特殊声明(匿名结构体):
今天我们关于声明部分要补充的,是关于结构体的不完全声明,即匿名结构体类型:
struct
//没有声明结构体标签,即为匿名结构体类型
{
char name[20];
int age;
char sex[5];
float score;
}student = { "Zhang",21,"Man",91.7 };
//匿名结构体类型必须在生声明的同时进行定义
我们把这种在声明时省略掉结构体标签的结构体称为匿名结构体类型,在使用这种方式进行声明时,由于没有声明结构体标签,导致一旦该结构体结束声明,将无法再次进行定义,所以对于该类型的结构体来说,就必须在声明结构体的同时进行定义(可以不初始化)。
而我们再来看下面这个例子:
//结构体类型1:
struct
{
char name[20];
int age;
char sex[5];
float score;
}x;
//结构体类型2:
struct
{
char name[20];
int age;
char sex[5];
float score;
}*p;
- 在这个示例中,虽然两个结构体类型内的结构体成员完全一样,但因为两者都使用了匿名结构体的声明方式,编译器会把上面的两个声明当成完全不同的两个类型,于是在下面的代码中将被视为非法:
p = &x;
//一种类型的指针指向另一种不同类型,将被视为非法
- 而上面进行完全声明的 “ 学生 ”的示例中的s1~s6这六个结构体变量因为声明时声明了结构体标签(tag),所以会被视为同一种类型进行处理和调用。
1.4:结构的自引用:
顾名思义,结构的自引用就是指结构体在自己的声明中引用了自己的一种声明方式。
我们在结构体中套用结构体是可行的,但是像链表这一类的要存放下一个元素消息的,可不能直接存储下一个元素的所有消息,自引用中又嵌套了对自身的引用,如此循环往复,非常耗内存。所以,我们用存放指针的形式减少内存的消耗。
正确的方式是这样的:
struct Node
{
int data;
struct Node* next;//结构体的声明里面包含一个同类型的结构体指针
};
当我们在进行结构体变量的定义时同样进行了自引用,不同的是这一次我们使用了一个指针,指向了下一个结构体变量的空间,而在这次指向之后,指针指向的空间被固定,不再指向其它空间,如此就实现了真正的结构体自引用。
同时,我们还可以结合关键字 typedef 进行使用:
typedef struct Test
{
int data;
struct Test* NEXT;
//但在这里必须仍使用struct Test
//在结构体声明结束后才会进行重命名
}Test;
//使用tepydef关键字,将struct Test类型重命名为Test类型
//typedef struct Test Test;//第二种重命名方式
int main()
{
Test n;
//经过重命名,在进行定义时可以直接使用重命名后的类型名进行定义
return 0;
}
1.5:结构体变量的定义和初始化:
1.5.1结构体变量的初始化:
struct Point
{
int x;
int y;
}p1={0,0};
struct Point p2 = {1,2}; //初始化
int main()
{
struct Point p3 = {3,4};//初始化
return 0;
}
1.5.2结构体嵌套结构体的初始化
struct Point
{
int x;
int y;
}p1={0,0};
struct Point p2 = {1,2}; //初始化
struct S
{
int num;
char ch;
struct Point p;//嵌套一个struct Point类型的结构体p
float d;
};
int main()
{
struct Point p3 = {3,4};//初始化
struct S s1 = { 20,'w',{1,2},3.14f };//结构体嵌套结构体的初始化
struct S s2 = { .ch = 'w',.d = 3.14f,.num = 20,.p.x = 1,.p.y = 2 };//乱序初始化
printf("%d %c %d %d %f\n", s.num, s.ch, s.p.x, s.p.y, s.d);//打印s
printf("%d %c %d %d %f\n", s2.num, s2.ch, s2.p.x, s2.p.y, s2.d);//打印s2
return 0;
}
1.6:结构体的内存对齐(超重点!!★★★):
1.6.1:结构体的数据在内存中如何存储的?
经过上面的学习,我们就已经基本掌握了结构体的使用了。接下来我们将要深入研究结构体大小的计算过程,即结构体内存对齐,而这也是近年来许多公司面试与笔试中的热门考点。
我们先来看看下面这段计算结构体变量大小的代码:
struct S1
{
char c1;
int i;
char c2
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
//结果:
12
8
这里计算结果显示struct S1的大小是12个字节,而struct S2的结果是8个字节,为什么是这样的结果呢?
要想弄清楚究竟是如何进行结构体变量大小计算的,我们首先得掌握结构体的对齐规则:
- 第一个成员在结构体变量偏移量为0的位置(偏移量:该成员的存放地址与结构体空间起始地址之间的距离)
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数=编译器默认的一个对齐数与该成员自身大小的较小值
- vs中的默认对其数是8,Linux和gcc都没有默认对齐数,对齐数就是成员自身大小
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体成员中的对齐数)的整数倍
- 结构体成员中如果有数组,可以这样来理解:比如结构体中有一个int c[5];,其实这就相当于这个结构体中放了5个int型的变量。
知晓了结构体的对齐规则,我们再回过头来分析上面的结构体变量大小计算过程。下面,我们以结构体变量 test1 中为例展示结构体的对齐存储。
- 第一个成员为占据一个字节的 char 类型变量 a,我们按照规则将其放置在偏移量为0,即结构体空间的起始位置.
- 第二个成员为占据4个字节的 int 类型变量 b,按照规则我们首先要计算它的对齐数,我们将变量 b 的大小4与对齐数默认值8进行比较,得出较小值为4,即对齐数为4,于是我们将它放在对齐数的整数倍处,即最近位置第四字节处。
- 第三个结构体成员占据1个字节的 char 类型变量 c,同样按照规则我们计算出它的对齐数为1,并将它放在对齐数的整数倍处,即最近位置第九字节处:
- 最后,根据规则,结构体的总大小为最大对齐数的整数倍,而这三个变量中,对齐数最大的是 int 类型变量的对齐数4,则总大小应当为4的倍数。而既为4的倍数,又要能够容纳所有的结构体成员,最小的结构体大小应当为12个字节,即为结构体变量 test1 的大小。
同理各位小伙伴们下去以后可以自己尝试推算结构体变量 test2 的大小并进行验证。
1.6.2:为什么存在内存对齐?
但是我们发现,这样的方式造成了很大程度上的空间浪费,以 test1 为例,12个字节的大小中有六个字节的空间申请了但却没有被使用。那么为什么还要采用这样的办法呢?主要有以下两个原因:
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说,结构体的内存对齐是拿空间来换取时间的做法
那么在设计结构体的时候如何实现节省空间呢?
让占用空间小的成员尽量集中在一起。
1.7:修改默认对齐数:
在我们的代码编写过程中,默认的对齐数可能会不够合适。而当这个时候,我们就可以通过使用下面这个预处理指令来修改我们的默认对齐数:
#pragma pack(8)
//修改默认对齐数为8
我们也可以通过该指令在修改过默认对齐数之后,取消设置的默认对齐数,将其还原:
#pragma pack()
//取消设置的默认对齐数,还原为默认
1.8:结构体传参:
结构体传参与函数传参类似,没有什么疑难点,我们直接来看下面的示例:
//结构体传参
struct S
{
int data[4];
int num;
};
//直接传结构体变量
void print1(struct S s)//实参传的是一个结构体变量,所以形参用一个对应的结构体变量来接收
{
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", s.data[i]);
}
printf("%d\n", s.num);
}
//传结构体的地址
void print2(struct S* ps)//实参传的是结构体变量的地址,所以形参用一个对应类型的结构体指针来接收
{
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3,4}, 1000 };
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
而在上面这段代码中,我们一般认为 Print2 函数更为优秀。原因是当函数传参的时候,参数是需要压栈的,在这个过程中就会产生时间和空间上的系统开销。如果传递一个结构体对象时结构体过大,那么将会导致参数压栈的的系统开销较大,最终将会导致程序性能的下降。
二:位段
2.1:什么是位段?
位段(bit-field)以位为单位来定义结构体(或联合体)中的成员变量所占的空间。含有位段的结构体(联合体)称为位段结构。采用位段结构既能够节省空间,又方便于操作。
位段的声明和结构体十分相似,但同时有两个不同点:
- 位段的成员必须是int、unsigned int、signed int等整型家族的类型。(大部分平台也支持char类型。一个位段的所有成员类型一般都是相同的)
- 位段的成员名后边有一个冒号和一个数字(该成员所占内存空间大小,单位为 bit位)。
#include<stdio.h>
struct test
{
int _a : 2;
//成员 a 只占用 2 个比特位
signed int _b : 5;
//成员 b 只占用 5 个比特位
unsigned int _c : 10;
//成员 c 只占用 10 个比特位
char _d : 4;
//成员 d 只占用 4 个比特位
};
int main()
{
printf("%d\n", sizeof(struct test));
return 0;
}
采用位段结构的好处是能够节省大量的空间,通过有条件地(根据实际使用需求)限制每个变量所占内存空间的大小,从而减少了整体结构的空间占用.
2.2:位段的内存分配
首先我们需要知道,位段中的"位"指的是二进制位。int _a : 2; 表示给 _a 变量分配2个bit位();int _b : 5; 表示给 _b 变量分配5个bit位,以此类推:给== _c== 变量分配10个bit位,给 _d 变量分配30个bit位。
结构体能干什么位段就能干什么,位段只是比结构体更节省空间而已。
-
位段的成员可以是 int 、unsigned int 、signed int 或者是 char(属于整形家族)类型
-
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
-
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
-
int 的位段的内存分配是逐4字节(一个 int 类型的大小)进行分配的。
-
当字节内空间不足以放下下一个成员变量时,剩余的空间不再使用,而是再从内存中申请一个字节的空间继续分配。
struct A的位段成员都是int型,所以会按照4个字节来开辟空间,首先开辟第一个4字节的空间(32bit),_a占用其中的两个bit位,_b占用里面的五个bit位,_c占用里面的十个bit位,此时,第一次开辟的4个字节(32bit)只剩下15bit了,而_d需要30bit,因此空间不够,需要再开辟第二个4字节空间(32bit),所以struct A这个类型的大小就是8个字节(64个bit位)。
2.3:验证vs2019上位段的内存分配和使用
2.4:位段的跨平台问题
我们上面说过,位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段,并且在未来位段结构的使用过程中,我们一定要提前仔细地研究好位段在不同编译器下使用时,究竟是如何进行内存分配的,再结合我们的实际需求实现跨平台使用。
而在位段进行跨平台使用时,我们通常需要注意以下四个关键点:
-
int位段被当成有符号数还是无符号数是不确定的
-
位段中最大位的数目不能确定。(比如 int 型在16位机器中最大只有16bit,在32和64位机器上最大有64bit)
-
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
-
当一个结构体包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的 bit 位时,是舍弃剩余的位还是利用,这也是不确定的
总结:
跟结构相比,位段可以达到跟结构相同的效果,并且可以更好的利用空间,但同时存在着跨平台问题
2.5:位段的应用
位段会被应用在网络数据传输中,它可以有效缓解网络“拥堵”。
总结:
今天,我们从结构体的一般声明开始,了解了一种特殊的声明(匿名结构体),学习了结构体变量的定义和初始化,探究了结构体的内存对齐,并介绍了修改默认对齐数方法,最后介绍了一种特殊的结构体——位段,它的最大优点是比普通的结构体更加节省空间。
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕😘😘💕 ~ ~ ~ 你们的点赞和关注对我真的很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!
专栏订阅:
每日一题
c语言学习
算法
智力题
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!