个人主页:_麦麦_

今日名言:生活不可能像你想象的那么好,也不会像你想象的那么糟。——莫泊桑《羊脂球》

目录

一、前言

二、正文

1结构体

1.1结构体的基础知识

1.2结构的声明

1.3特殊的声明

1.4结构体变量的定义和初始化

1.5结构的自引用

1.6结构体内存对齐

1.7修改默认对齐数

1.8结构体传参

2.位段

2.1什么是位段

2.2位段的内存分配

2.3位段的跨平台问题

2.4位段的应用

三、结语


一、前言

好久不见,今天为小伙伴们带来C语言中有关结构体的详细知识,干货满满,图文并茂一定要看到底哦!

二、正文

1结构体

1.1结构体的基础知识

结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量

1.2结构的声明

struct tag

{

member—list;

} ;

注:分号不能丢

//结构的声明演示:描述一个学生struct Stu{char name[20];//名字char sex[5];//性别int age;//年龄char id[20];//学号};

1.3特殊的声明

在声明结构的时候,可以不完全声明【匿名结构体类型】,由于匿名,所以声明后就得在后面直接创建变量。

//匿名结构体类型struct{int a;char b;float c;}x;//创建变量x

注:哪怕两个匿名结构体中的内容完全一样也会被编译器当成两个完全不同的类型

//匿名结构体类型1struct{int a;char b;float c;}x;//匿名结构体类型2struct{int a;char b;float c;}*p;//非法操作p = &x;

1.4结构体变量的定义和初始化

结构体变量的定义共分为三类:

①全局变量定义

②局部变量定义

③在结构的声明的同时定义变量

//结构体变量的三种定义方法struct Stu{char name[20];//名字char sex[5];//性别int age;//年龄char id[20];//学号}Stu1;//声明结构的同时定义变量struct Stu Stu2;//全局变量定义int main(){struct Stu Stu3;//局部变量定义return 0;}

结构体变量的初始化即定义变量的同时赋初值。不过结构体的初始化也分为正常的初始化嵌套初始化

//结构体变量的初始化struct Peo{char name[20];//名字char sex[5];//性别int age;//年龄};struct PeoPeo1 = { "陈书婷","女",35};//结构体正常初始化struct Node{char movie[20];struct Peop;}Peo2 = { "狂飙",{"高志强","男",40 }};//结构体嵌套初始化struct Node Peo3 = { "狂飙",{"高启盛","男","28"}};//结构体嵌套初始化

注:结构体的初始化其实可以更加灵活——乱序,按照自己想法来初始化。

struct Peo{char name[20];//名字char sex[5];//性别int age;//年龄};struct Peo Peo4 = { .sex = "男",.name = "安欣",.age = 30 };

1.5结构的自引用

采取指针的形式

在结构中包含一个类型为该结构本身的成员

//自引用1(错误示范)struct Node{int date;struct Node next;};//自引用2(正确示范)struct Node{int date;struct Node* next;};

1.6结构体内存对齐

在小伙伴们掌握了结构体的基本使用之后,接下来让我们深入讨论一个问题:如何计算结构体的大小?为了解决这个问题,我们就必须掌握结构体的对齐规则。

●第一个成员在与结构体变量偏移量为0的地址处

●其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

对齐数=编译器默认的一个对齐数与该成员大小的较小值

★VS中默认的值为8

●结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

●如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

#include//结构体内存对齐代码1struct s1{double d;//对齐数8char c;//对齐数1int i;//对齐数4};//结构体内存对齐代码2struct s2{char c1;//对齐数1struct s1 s1;//对齐数8double d;//对齐数8};int main(){printf("%d", sizeof(struct s1));//16printf("%d", sizeof(struct s2));//32return 0;}

结构体内存对齐代码1:首先我们将“d”成员放入内存中,由于它是第一个成员,且对齐数为8,所以从偏移量为0的地址一直放到偏移量为7的地址。其次是“c”成员,它的对齐数为1,因此可以放在偏移量为8的地址处,最后是“i”成员,它的对齐数是4,但是偏移量为9的地址并不是对齐数4的倍数,所以我们只好跳到偏移量为12的地址处,直至偏移量为15的地址处才放下”i”成员。在将所有的成员放入后,就是计算结构体s1的大小,由于15并不是最大对齐数8的倍数,所以结构体s1的大小为16个字节。

结构体内存对齐代码2:首先我们将“c1″成员放入内存中,由于它是第一个成员,且对齐数为1,所以放入偏移量为0的地址处。其次是“s1″成员,该嵌套结构体内的最大对齐数为8,因此跳到偏移量为8的地址处开始存放double类型成员,直至偏移量为15的地址处才存放完毕。接着是char类型成员,对齐数为1,存放在偏移量为16的地址处。继而是整型成员,对齐数为4,从偏移量为20的地址存放,直至偏移量为23的地址处存放完毕。最后是“d”成员对齐数为8,从偏移量为24的地址存放,直至偏移量为31的地址处存放结束。在将所有的成员放入后,就是计算结构体s2的大小,由于31并不是最大对齐数8的倍数,所以结构体s2的大小为32个字节

最后总结一下计算结构体大小的步骤

计算出所有成员的对齐数并得出结构体的最大对齐数

根据每个成员的对齐数依次存放每个成员

所有成员存放完毕后,依据结构体的最大对齐数得出结构体大小

那么如何证明我们对结构体成员的偏移量计算是否正确呢” />#includeint main(){printf(“%d\n”, offsetof(struct s1,d));printf(“%d\n”, offsetof(struct s1,c));printf(“%d\n”, offsetof(struct s1,i));printf(“%d\n”, offsetof(struct s2, c1));printf(“%d\n”, offsetof(struct s2, s1.d));printf(“%d\n”, offsetof(struct s2, s1.c));printf(“%d\n”, offsetof(struct s2, s1.i));printf(“%d\n”, offsetof(struct s2, d));return 0;}

在了解完结构体的内存,可能有的小伙伴会发出如下的疑问:为什么会存在内存对齐呢,这到底有什么用呢?

大部分的参考资料都是如是说的:

1.平台原因(移植原因):

不是所有的硬件平台都能任意访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则就会抛出硬件异常

2.性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界对齐。

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说:结构体的内存对齐是拿空间来换取时间的做法

那么在设计结构体的时候,我们既要满足对齐,又要节省空间,该如何做呢?

占用空间小的成员尽量集中在一起

1.7修改默认对齐数

那么默认对齐数可以修改吗?答案是肯定的。在C语言中存在#pragma这个预处理指令,通过这个我们就可以改变默认对齐数了。

//修改默认对齐数#include #pragma pack(2)struct s1{char c1;int i;char c2;};

注:如果将默认对齐数设置为1,则不存在对齐效果。在平常的使用中,小伙伴们一定要根据实际需求修改默认对齐数

1.8结构体传参

在之前的指针学习中我们了解到了”传值调用“和”传址调用“这两个概念,那么在结构体传参时依旧存在以上两种方式,那么那种方式是最优选择呢?

//结构体传参#include struct s{int date[1000];int num;};struct s s1 = { {1,2,3,4},666 };void print1(struct s s1){printf("%d",s1.num);}void print2(struct s* s1){printf("%d", s1->num);}int main(){print1(s1);//传结构体print2(&s1);//传地址}

其实,在结构体传参这一步中传结构体的地址是一个更好的选择。因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

2.位段

在结构体讲完之后就要向小伙伴们介绍结构体实现位段的能力

2.1什么是位段

位段的声明和结构是类似的,却能更加的节省空间,但是有两个地方存在差异:

①位段的成员必须是int、unsigned int 或 signed int

②位段的成员名后边有一个冒号和数字【表示占几个二进制位】

#include struct A{int _a : 2;//a只占2个二进制位int _b : 5;//b只占5个二进制位int _c : 10;//c只占10个二进制位int _d : 30;//d只占30个进制位};int main(){printf("%d\n", sizeof(struct A));//打印为8个字节,是不是更节省空间了呢return 0;}

2.2位段的内存分配

1.位段的成员可以是int 、unsigned int、signed int、或者是char(属于整形家族)类型

2.位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的

3.位段设计很多不确定因素,位段是不跨平台的,注意可移植的程序应该避免位段

4.位段是不存在内存对齐的

注:在不同的编译器中,同一位段的大小也是不确定的,接下来我们以VS2019的环境下解释上述位段的大小为何为8个字节。

在理解完位段在VS下的内存开辟,那么内存是如何使用的呢?在VS的环境下,内存开辟后是从右向左使用的且为小端存储,依旧以上面的代码为例:

2.3位段的跨平台问题

①int位段被当成有符号数还是无符号数是不确定的

②位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题)

③位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义

④当一个结构包含两个位段,第二个位段成员比较大,无法容纳与第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

总结:跟结构相比,位段可以达到同样的效果,但是可以很好地节省空间,但是有跨平台的问题存在

2.4位段的应用

大家都知道彼此之间相互的交流看似简单,其实文字交流的背后是大量的网络数据,而位段的使用恰好可以对数据进行压缩,减少网络的压力和负担。形象的来说,我们可以把网络想象成高速公路,如果上面全是未经压缩的数据,也就都是大卡车的话,就会十分拥挤。而如果采取位段的方式,对没有必要使用的空间进行压缩,就可以将大卡车变成小轿车,从而缓解交通压力。

三、结语

关于结构体的讲解就已经全部结束了,下期我们会继续分享自定义类型的其他成员!

关注我 _麦麦_分享更多干货:_麦麦_的博客_CSDN博客-领域博主
大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!