目录
🥎什么是结构体?
⚾结构体的声明
🏀简单结构体的声明
🏐结构体的特殊声明
🏈结构体嵌套问题
🏉结构体的自引用
🎳结构体的内存大小
🥌结构体的内存对齐
⛳内存对齐的优点
⚽还记得之前我们讲过的结构体吗?当时我们只是简单的认识了一下结构体应该如何书写以及到底应该怎样传参。今天,我们就来详细介绍一下我们的结构体。话不多说,就让我们开始我们今天博客的主要内容吧!
🥎什么是结构体?
⚽首先我们来认识一下什么是结构体呢?说到结构体就不得不和我们的数组进行类比了。所谓的结构体就是:结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。确实结构体的概念和我们数组的概念很像,但是有一点需要我们特别注意:数组是一组相同的数据的集合体,但是我们的结构体是一组不同数据的集合。举一个简单的例子来说:数组就像是一个肠道很脆弱的宝宝,这个宝宝只能喝粥,其他什么也吃不下。所以我们只能喂这个小宝宝喝粥。我们的数组中的元素类型都取决于前面的变量类型所决定,一旦决定之后我们的数组终究只能放同一种元素。而我们的数组就像是身体状况的恢复健康的青年人,可以吃的下任何东西。同样的道理我们的结构体当中可以存放不同类型的元素。所以使用结构体进行描述我们生活中具有多中特点的数据比较容易。那么接下来就让我们 看一看结构体的书写方式吧!
⚾结构体的声明
🏀简单结构体的声明
⚽那么我们就通过一个简单的例子来认识一下结构体的书写形式:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
⚽我们的学生就像是我们所说的那样,并不是一个简单的统一元素所能进行定义的。具有名字,年龄,性别等特点,所以我们需要将我们的学生的信息构建成为一个结构体的形式。在结构体中,我们就像是构建变量的方式进行构建一个 char 类型的数组,以及 int 类型的变量等来丰富这个结构体的特点。那我们已经向大家说明了结构体的类型,那么我们接下来再来向大家介绍一下这样的结构体详细的使用方法。
⚽结构体的使用方法:
#include<stdio.h>
struct Stu //结构体类型
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
//也可以在这里创建结构体变量
//在任何函数之外创建的结构体变量为全局变量,什么时候都可以使用
//struct Stu s1 = { "张三",18,"男","A123456" };
int main()
{
struct Stu s1 = { "张三",18,"男","A123456" };//创建具体的结构体变量
//假设我们想打印结构体我们所创建的元素
printf("%s %d %s %s", s1.name, s1.age, s1.sex, s1.id);
return 0;
}
⚽就像是我们上面的代码所运行的效果所表示的那样:我们可以创建全局的结构体变量也可以创建部分函数内部的结构体变量。(我们刚开始创建的只是结构体的类型,系统不会为其开辟内存,想要使用必须重新创建结构体变量)同样的我们还可以在结构体的类型创建好的时候再大括号的后面直接紧跟着创建一个结构体变量。
struct Stu //结构体类型
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1; //分号不能丢
//在结构体类型创建好的时候紧跟着也可以创建结构体变量。
//在这里创建的结构体变量同样为全局变量
⚽就像是我们上面的例子一样。我们在结构体类型创建好时创建的结构体变量同样为全局变量,在任何地方都可以使用。上面的结构体变量在创建好之后我们要想使用只需要利用我们能的变量名即 s1 之后再通过 . 操作符找到我们结构体中所指向的变量的元素即可。
🏐结构体的特殊声明
⚽接着要介绍的就是我们结构体的特殊声明。我们先通过例子有一个初步的了解。
struct
{
int a;
char b;
float c;
}x;
⚽像是上面的声明结构体的方式相信大家都发现了和我们普通结构体不一样的地方:那就是没有结构体的标签。没错,我们要介绍的特殊的结构体的声明方式就是我们的匿名结构体。就像是我们上面的例子一样,我们的匿名结构体在创建结构体类型没有结构体的标签,这就意味着,我们之后不能通过结构体的标签找到我们这次创建的结构类型进行重新添加结构体变量。所以我们这样创建的结构体只能在创建好结构体类型的时候就添加上结构体变量,只能进行有限次使用。之后就会被废弃,其使用的方法和我们正常的结构体类型一样。所以当我们想要创建的一个只能使用一次的结构体变量的时候我们就能使用匿名结构体进行编写我们的程序。
🏈结构体嵌套问题
⚽要是经常阅读代码的小伙伴们一定会遇到像是结构体的嵌套的问题。所谓的结构体的嵌套举一个简单的例子来说就是:我们的学校同样是一个复杂的对象,所以我们想通过结构体来构建一个School的结构体类型,我们的School结构体类型当中肯定会包含学生等信息。那么这又是一个复杂的对象我们需要重新创建一个Stu的结构体类型。像这样我们在School的结构体类型中包含了另一个结构体的形式就叫做结构体的嵌套。那么我们利用代码来深度认识一下结构体的嵌套应该怎么表示。
#include<stdio.h>
struct Student
{
int age;
char name[20];
};
struct Teacher
{
int age;
char name[20];
};
//结构体的嵌套问题
struct School
{
int teacherNumber;
int studentNumber;
struct Teacher t1; //引用外部结构体
struct Student s0; //引用外部结构体
};
int main()
{
struct School s1 = { 53,1380,{34,"王美丽"},{14,"李明"} };
printf("%d\n%d\n%d\n%s\n%d\n%s", s1.teacherNumber,//53
s1.studentNumber,//1380
s1.t1.age,//34
s1.t1.name,//王美丽
s1.s0.age,//14
s1.s0.name);
return 0;
}
⚽在我们使用结构体的嵌套的时候我们需要将我们内部的结构体创建成一个结构体变量的形式,以便于我们可以通过 . 操作符进行查找指定元素。
🏉结构体的自引用
⚽我们在上面刚讲完结构体的嵌套问题相信有的小伙伴们可能会有一个疑问:在结构中包含一个类型为该结构本身的成员是否可以呢?回答是当然可以。但是这其中有一个小小的坑——我们的结构体的自引用的形式还是像是我们结构体的嵌套那样直接在结构体的内部创建一个自己的结构体变量吗?我们可以通过一张图片梳理一下思路:
⚽我们会发现的是我们要是利用和我们结构体进行自嵌套的话我们的结构体就会出现死递归的情况,这样的话我们的程序就会出现问题。那么我们的结构体究竟应该怎样进行自引用呢?我先来给大家介绍一下数据结构中的一个概念——链表。链表的设计就和我们的结构体的设计如出一辙。
⚽在我们的链表中我们每一个想要存储的数据都分为两部分:1.数据部分 2.下一个元素的地址 我们可以通过下一个元素的地址找到我们下一个元素。我们在链表中每一个节点的设计就是利用了我们结构体的自引用。让我们来想象一下:要是我们在我们结构体中想要找到并引用自己本身,那么只需要包含我们的这个结构体的指针即可。我们之后就可以通过这个指针的解引用等操作进行再次使用我们这个结构体。
⚽这样的话要是我们就可以通过自主进行控制函数的使用次数了。是不是很神奇?我们再来通过一段简单的代码进行更加深度的认识:
#include<stdio.h>
struct Node
{
char elem1;
int elem2;
struct Node* s1;
};
int main()
{
struct Node test1 = { 'a',12 };
struct Node test2 = { 'b',18 };
test1.s1 = &test2;
printf("%c %d %c %d", test1.elem1,
test1.elem2,
test1.s1->elem1,
test1.s1->elem2);
return 0;
}
⚽就像我们上面的代码所展示的那样我们的结构体在自引用的时候需要创建至少两个结构体变量,并将其中一个变量的指针交给我们的结构体里另一个结构体变量中的指针,之后我们就可以通过 -> 指针查找的方式找到相应的元素内容啦!
🎳结构体的内存大小
⚽为了让我们大家更加详细的了解我们结构体的执行原理,所以我们在这里补充一部分的内容——结构体的内存大小的判断方法。首先我们通过一个现象进行一步步的分析。
⚽看到我们得到的内存的大小肯定小伙伴们会有很多的疑问。首先我们的结构体中有三个元素类型,按道理来说总和大小应该为6个字节才对,为什么打印出来的结构体的大小没有一个是6呢?其次,我们所创建的两个结构体中的元素都是一样的但是为什么两个结构体的大小却不相同呢?为了解决我们的疑惑我先来给大家介绍一点:在结构体存储的时候存在内存对其。
🥌结构体的内存对齐
⚽那么什么是结构体的内存对齐呢?我们先来介绍一下内存对齐的规律:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体 的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
⚽上面的四点就是我们结构体内存存储的重要规律。我们一一来解释:我们的结构体在内存中存储的时候会开辟一块空间,这个空间的第一个地址我们把它叫做我们结构体的0偏移量处。
⚽因此我们的结构体总是从0偏移处开始存储的。第二点就是其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。在我们的VS中默认的对齐数为8。举一个简单的例子来说我们上面创建的第一个结构体中第一个元素为char 类型所以占一个字节,即我们的0偏移的位置,下一个元素为 int 类型所占大小就为 4 个字节,所以我们的 int 的对齐数就为 4 和我们的默认对其数进行比较:4<8 所以我们取较小值,就是4,所以我们的第二个 int 类型的元素就要对齐到对齐数为 4 的倍数的位置处。(像是4,8,12.....)最后一个 char 类型的元素同理(所占字节大小为1,1<8,所以对齐数就为1,需要对齐到偏移量为一的倍数的位置处)。也就是我们下面的情况:
⚽但是要是我们会发现这也不对呀?这么算不是也只有10个字节的大小吗?为什么我们第一个大小求得是12呢?因为我们还没有分析完,我们接着进行规则的分析:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。等我们将我们结构体中的元素全部都放置到应该处于的位置的时候,我们得到的所占内存的大小还应该跟我们的最大的对齐数进行比较,我们结构体所占的内存的大小还应该使我们最大对其数的整数倍!我们看接着我们上面的例子进行分析:我们上面求到的大小为10个字节,我们上面所出现的对齐数的大小为1,4,1,那么我们最大的对齐数就是4。我们最后得出的答案就需要是 4 的整数倍。就会寻找最近的4的倍数作为结构体的内存大小。也就是我们的12。这么看来我们的结论刚好用上。那么我们来探究一下下面的结构体为什么大小为8吧!
⚽1.第一个元素为char在我们的0偏移处,第二个元素char在我们的1偏移处,第三个元素int从我们4偏移处开始一直到我们的7偏移处结束。最后最大对齐数为4,找到最近的4的倍数作为我们结构体的大小也就是8。是不是感觉很简单?我们对齐数的第四条规律其实是一样的解释形式。我们需要将我们嵌套在内部的结构体的大小求出来进行和8进行比较作为存储偏移量的位置。最后求得嵌套结构体在内存中所占的大小。
⚽但是肯定会有小伙伴好奇:那些其他的地址呢?其实那些没用到的地址都浪费掉了。那么接下来我就向大家解释一下我们这种内存的浪费意义在哪里。
⛳内存对齐的优点
⚽内存对齐的主要优点有两个:1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。简单一点来说就是由于我们计算机的硬件的不同所以有的计算机就不能随意的读取数据,就像我们的结构体,刚读完一个一个字节的 char 紧接着就要重新读四个字节的 int 我们的计算机硬件的配置设计起来可能会很复杂,所以降低成本就会产生内存对齐的现象,告诉计算器你把内存中的全部数据都当作最大的那个数据进行读取。这样就避免了我们内存读取的来回转换的问题。
⚽2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。我们可以通过画图的形式来理解认识我们这一点:
⚽我们知道的是我们的计算机现在主要分为32位机器和64位机器。32位机器一次读取只能读取四个字节。那么要是我们没有内存对齐的话要是想要读取我们第二个 int 类型的数据就需要读取两次,之后将两次读取的数据进行拼接才可以的到我们想要的数据。但是我们存在内存对齐的机器只需要读取一次就可以了。这是典型的用空间换时间的问题。可以很大程度上提高我们程序的运行效率。
⚽那么到上面为止我们的结构体的相关知识的详细解析也就结束了。希望大家有所收获,感谢您的观看,祝您天天开心。