什么是结构体【详解】

2023-05-16

请添加图片描述

本期介绍🍖

主要介绍:什么是结构体,结构体的声明、定义、初始化、以及传参,匿名结构体类型,如何通过结构体来实现链表数据结构,结构体在内存中是如何存储的(即:结构体内存对齐),什么是内存对齐👀。


文章目录

    • 一、什么是结构体🍖
    • 二、结构体的声明、定义、初始化🍖
    • 三、匿名结构体类型🍖
    • 四、结构体自引用🍖
    • 五、结构体内存对齐🍖
      • 5.1 结构体对齐规则🍖
      • 5.2 为什么存在内存对齐🍖
      • 5.3 修改默认对齐数🍖
      • 5.4 成员在内存中在存储顺序🍖
    • 六、结构体传参🍖


  我们知道C语言本身就存在着一些语法类型,也就是内置类型譬如:char、short、int、long、float、double)。但在实际应用中仅仅只有这些类型是不够用的,因为我们无法单一的用内置类型来描述一个复杂的对象,就譬如:一个人、一本书等等。所以C语言就给出了除内置类型之外的一种类型:自定义类型,有了这种类型我们就能够自己来构建类型了。而比较常用的自定义类型有:结构体联合体枚举


一、什么是结构体🍖

  结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员的数据所组成,其中每个成员可以是不同类型的。结构体通常用来表示类型不同但是又相关的若干数据。(注意:数组与结构体一样,也是一种值的集合,只不过数组的每个元素都要是相同类型的,而结构体的每个成员可以是不同类型的


二、结构体的声明、定义、初始化🍖

  首先,要使用结构体就必须声明(创建) 一个结构体类型,就必须用到结构体关键字struct。如何声明一个结构体类型,如下所示。其中tag结构体标签,用来区分不同的结构体类型。struct tag表示结构体类型,声明完结构体后就可以用该类型来创建变量了。member-list;成员列表,它是结构体所包含的基本的结构类型。variable-list;表示变量列表,这个列表可写可不写,写了就代表你用上面所创建的结构体类型定义了一个该类型的变量,没写则表示你仅仅只创建了一个结构体类型。

struct tag
{
	member-list;
}variable-list;

注意:
 1.若结构体声明是在mian函数之外的,那么列表后创建的结构体变量是一个全局变量
 2.结构体成员变量也可以是另一个结构体类型,这种用法被称为:嵌套结构体
 3.在结构体声明结束的时候,千万不要忘记{}后面是有一个的。


  声明完结构体类型,就可以用它来定义初始化结构体变量了。注意:结构体初始化与数组一样需要用{}。举个例子,如下图所示:

struct grade
{
	double math;
	double english;
};
struct student
{
	char name[20];//姓名
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
	struct grade;//成绩(这是一个嵌套结构体类型)
};
int main()
{
	//定义、初始化结构体类型
	struct student ly = {"张三", 23, "男", "2117305789", {98.5, 66.0}};
	return 0;
}

  不知道大家在用结构体类型定义一个变量时,有没有觉得结构体的类型名太长了。其实可以通过typedef来重定义结构体的类型名,如下所示:

typedef struct student
{
	char name[20];//姓名
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
	struct grade;//成绩(这是一个嵌套结构体类型)
}stu;

  注意这里的stu并不是创建的变量,而是对于struct student的重命名。之后我们既可以用stu来创建变量也可以用struct student来创建。


三、匿名结构体类型🍖

  我们在声明结构的时候,可以不完全的声明,也就是不写结构体标签,所以称之为匿名。如下所示:

struct
{
	int a;
	char b;
	double c;
}x;

  这种语法结构C语言是支持的,但不被我们所喜,因为这种声明出来的结构体类型我们只能用其定义一次变量,之后想用都用不了了。为什么呢?难道我们之后要用struct类型来创建变量吗?别扯淡了,struct可是结构体关键字啊!总之,除非你只想用一次你创建的结构体类型,或者不想该结构体类型被别人使用,这时可以用匿名结构体类型。

  还值得注意的一点是,就算两个匿名结构体类型的成员变量完全一样,但在编译器看来它们两个仍然是两个完全不同的类型。如下举例所示:

struct
{
	 int a;
	 char b;
	 float c;
}x;

struct
{
	 int a;
	 char b;
	 float c;
}a[20], *p;

int main()
{
	p = &x;
	return 0;
}

在这里插入图片描述


四、结构体自引用🍖

  我们知道在数据在内存中有许多存储的方式,就譬如:顺序表链表等等。其中顺序表是在内存中连续存放的,而链表是打乱着存放的,其并不连续。如下图所示:

在这里插入图片描述

  我们在读取数据的时候顺序表只要知道起始位置,就可以依次找到各个数据。但链表却不行啊,因为它在内存中并不是连续的,而是乱序存放的。问题来了,那链表该怎么往外取数据呢? 其实也不难,是不是只要1可以找到2,2可以找到3,3可以找到4,4可以找到5,不就可以把存在不同内存位置中的数据全部依次的取出来了嘛,数据就如同被一个链条串起来了一样,故称为:链表结构存储。那该怎么实现链表呢?


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

struct Node
{
 int data;
 struct Node next;
};

  其实我们只需要在结构体中包含一个类型为该结构体本身的成员,这样就可以像套娃一样,在一个数据中找到另一个数据,在另一个数据中又找到另另一个数据,以此下去。这样不就可以实现链表结构了嘛!假设这个想法可以吧,那再仔细思考一下,当使用sizeof()来计算该类型的结构体大小时,结果又是多少?

#include<stdio.h>
struct Node
{
 int data;
 struct Node next;
};

int main()
{
	printf("%d\n",sizeof(struct Node));
}

在这里插入图片描述

  可见,C语言是不支持结构体中包含一个类型为该结构体本身的这种语法,为什么呢?因为编译器也无法得知该结构体类型的大小呀,每个结构体套一个自身,这种思想不就是函数递归操作(没有限制条件的)嘛,理论上可以套无数个该类型的结构体,你说编译器能计算出它的大小吗?这压根就是一个错误的写法。所以我们无法通过结构体中包含下一个结构体这种写法来实现链表的,我们需要另辟思路了。


  想法二: 在结构中包含一个指向类型为该结构自身的指针

struct Node
{
 int data;
 struct Node* next;
};

  稍加思索,不难得出这个想法其实是可行的。如下图所示,我们只要就把2节点的地址赋给1节点,把3节点的地址赋给2节点,把4节点的地址赋给3节点,把5节点的地址赋给4节点,最后给5节点赋上一个NULL(表示不指向任何位置)。如此不就可以通过一个节点能够找到下一个节点这种方式逐个找遍所有节点了嘛,不就实现链表了嘛。

在这里插入图片描述
  而且该想法必不会像先前的想法那样,在计算类型所占内存大小时无法被获知。因为,这样设计的结构体类型的第二个成员变量本质上就是一个指针,而指针无非就是4/8个字节嘛,怎么可能会出现无法计算的情况呢?指针又不套娃!!!


五、结构体内存对齐🍖

  不知道大家在学习结构体的时候有没有思考过结构体它在内存中到底是如何存放的结构体多占内存真的是所有成员变量大小的总和吗?会与数组一样随着下标的增长地址由低到高变化吗?大家先来猜一猜,下边两个结构体S1和S2在内存中会占用多少空间:

#include<stdio.h>
struct S1
{
	char a;
	int b;
	char c;
};
struct S2
{
	char a;
	char b;
	int c;
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	return 0;
}

  这两个结构体所含的成员变量个数和类型都一样啊,都是由一个int和两个char类型构成的。那么按照一贯的思维,我们必然会得出这两个结构体理应都占用6个字节的内存空间啊。但事实真的是这样吗?

在这里插入图片描述

  答案当然是否定的,从上图就可以看出结果完全与我们所思所想的不同。有人就要问了:不因该啊,为什么会结果会得出大于结构体成员变量大小总和这种情况啊?这样做不就浪费内存空间了嘛! 而且还可以看出S1和S2两个结构体的成员仅仅只是顺序不同,最后得出的结果也是不一样的!那这到底是怎么回事呢?这就不得不好好说一说结构体在内存中的存放方式了(也就是结构体内存对齐)了。


5.1 结构体对齐规则🍖

  结构体对齐规则:

  1. 第一个成员在相对于结构体变量起始位置偏移量为0的地址处
    (通俗点来说,就是第一个成员变量的地址与结构体起始位置的地址是相同的)如下图所示:
    在这里插入图片描述
  2. 其他成员变量要对齐到<对齐数>的整数倍处
    (对齐数 = 编译器默认对齐数与该成员变量大小的较小值),vs编译器默认对齐数为8,gcc没有默认对齐数这一说。
    在这里插入图片描述
  3. 结构体的总大小为最大对齐数(每个成员的都有一个对齐数)的整数倍
    在这里插入图片描述
  4. 如果是嵌套结构体的情况,嵌套结构体的对齐数就是其自身的最大对齐数。(同理数组的对齐数就是其元素的对齐数)
    在这里插入图片描述

  在VS编译器中存在一个宏offsetof(type, member),可以用来计算一个成员在结构体类型所创建的变量中的偏移量是多少。其中type是指想要被计算的结构体类型,member是该结构体中的成员。注意:在使用offsetof()前应该先引用一个头文件<stddef.h>

#include<stdio.h>
#include<stddef.h>

struct S1
{
	char a;
	int b;
	char c;
};

int main()
{
	printf("%d\n", offsetof(struct S1, a));
	printf("%d\n", offsetof(struct S1, b));
	printf("%d\n", offsetof(struct S1, c));
	return 0;
}

在这里插入图片描述

  这与我们之前所述的情况几乎是一摸一样。


5.2 为什么存在内存对齐🍖

  通过上面的讲解,我们发现结构体在内存中存储时,会故意浪费掉一些空间,来使得结构体成员在某些边界处对齐。那为什么要这么做呢?原因有两个:

  1. 平台原因(移植原因):
    不是所有的硬件平台都能够访问任意地址处的任意数据的,某些硬件平台只能在某些地址处取特定类型的数据,否则就会抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)因该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的数据,处理器需要做两次内存访问,而对齐的内存只需要一次访问就够了。
    在这里插入图片描述

  总体上来说,结构体的内存对齐就是一种用空间换取时间的做法,浪费空间来换取性能上的提升。那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中到一起,就可以降低一些空间的浪费了举个例子:
在这里插入图片描述


5.3 修改默认对齐数🍖

  其实VS编译器中的默认对齐数(也就是8)是可以进行编译修改的,通过#pragma预处理指令来进行修改。举个例子:

#include<stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S1
{
	char a;
	int b;
	char c;
};

int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}
#pragma pack()//取消设置的默认对齐数,还原为默认

在这里插入图片描述

  结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。


5.4 成员在内存中在存储顺序🍖

  由下图可知,结构体成员变量在内存中存放的顺序是这样的:

在这里插入图片描述


六、结构体传参🍖

  结构体与普通内置类型一样也有两种传参方式:1. 传值2. 传址。那我们到底该选择哪一种传参方式更好呢?其实吧,选择传递结构体变量的地址更好,为什么呢?如果是传值函数在传参的时候参数是需要压栈的,若此时结构体过大,参数压栈时系统的开销也就较大,会导致性能的下降。可传址就不同了,不管你结构体有多大我传递的地址永远是4/8个字节,开销远比传值小的多。而且传值和传址都能够实现对结构体变量的调用,故首选传址调用。下面是两个不同的传参方法:

struct S
{
 int data[1000];
 int num;
};

//结构体传参
void print1(struct S s)
{
 	printf("%d\n", s.num);
}

//结构体地址传参
void print2(struct S* ps)
{
	 printf("%d\n", ps->num);
}

int main()
{
	 struct S s = {{1,2,3,4}, 1000};
	 print1(s);  //传结构体
	 print2(&s); //传地址
	 return 0;
}

在这里插入图片描述

  注意:如果是结构体变量地址,我们需要用->来访问结构体的成员;而如果是结构体变量名,我们需要用.来访问结构体成员。

  注意:相同类型的结构体变量,我们可以直接通过=来进行赋值操作,不需要像数组那样访问到每一个元素然后才能进行赋值,因为结构体是一个类型啊。


在这里插入图片描述

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

什么是结构体【详解】 的相关文章

  • Makefile文件的编写(实例详解)

    1 什么是Makefile xff1f 一个工程中的源文件不计其数 xff0c 其按类型 功能 模块分别放在若干个目录中 xff0c Makefile定义了一系列的规则来指定哪些文件需要先编译 xff0c 哪些文件需要后编译 xff0c 哪
  • 用C语言实现万年历的代码及思路(详细教程)

    万年历程序要求 xff1a 1 当选择1的时候 xff0c 输入年 xff0c 打印输入的这一年12月的日历 2 当选择2的时候 xff0c 输入年 月 xff0c 打印输入这一年这一月的日历 实现效果 xff1a 选择1时 span cl
  • 有符号数和无符号数参与运算时的问题

    陷阱题目 xff1a 下面的代码输出是 xff08 xff09 span class token macro property span class token directive hash span span class token di
  • 【Linux网络编程】基于UDP实现多人聊天室

    文章目录 一 UDP的概念1 1 UDP1 2 UDP特点 二 采用UDP实现多人聊天室原因三 多人聊天室项目功能四 实现多人聊天室项目流程分析4 1 前期准备4 1 1 定义结构体4 1 2 定义链表 4 2 多人聊天室服务器4 2 1
  • 【C++】C向C++的知识过度(上)

    文章目录 一 C与C 43 43 的区别1 1 C是面向过程的1 2 C 43 43 是面向对象的1 3 编译器的区别 二 C与C 43 43 默认代码的不同三 命名空间3 1 关键字 96 namespace 96 去定义自己的名字空间
  • 【C++】四种类型转换 | C++异常处理机制 | C++11新特性之右值引用和移动构造

    文章目录 一 C 43 43 中的四种类型转换1 1 静态类型转换1 1 1 C 43 43 中内置类型的转换1 1 2 C 43 43 中的有继承关系存在的场景下的类型强转 1 2 动态类型转换1 3 常类型转换1 4 解释类型转换 二
  • 【Linux操作系统】进程详解(上)

    文章目录 一 进程的定义二 进程的特征三 进程的组成及其作用四 进程控制块4 1 进程控制块定义4 2 task struct的内容 五 进程与程序的区别六 进程与线程的区别七 进程的种类八 PID8 1 PID定义8 2 特殊PID的进程
  • 【Linux操作系统】进程详解(下)

    文章目录 前言一 父子进程共用光标问题1 1 验证1 2 规避共用光标问题使用多进程拷贝同一个文件 二 进程相关函数2 1 getpid getppid函数2 2 exit exit函数2 3 wait waitpid函数 三 守护进程3
  • git补丁操作(git diff、git format-patch、git apply、git am)

    https blog csdn net qq 42138454 article details 119058431 实际使用补丁用的 xff1a patch p1 lt device nuwa diff 一 打补丁的两种方法 Git 提供了
  • 三、C++ 链接器 linker

    cilinking 从C 43 43 源码到可执行二进制的过程 compile文件之后进行链接 xff0c 找到每个符号 函数的位置 xff0c 并将其链接在一起 每个文件被编译成一个独立的 obj文件作为translation unit
  • 五、C++中的头文件 Header Files

    头文件 Header Files传统上用于声明某些函数类型 xff0c 以便可以用于整个程序中 例如 xff1a 在一个文件1中创建函数 xff0c 然后想在另一个文件2中使用 xff0c 尝试编译此文件2时 xff0c C 43 43 不
  • 六、如何使用VS调试代码、条件语句

    how to use Visual Studio to debug our code 查看程序断点 调试程序 1 设置断点 断点 xff0c 调试器将中断 xff0c break xff0c 程序调试是将会中断 xff0c continue
  • 七、如何设置C++项目 set up C++ project

    Visual Studio设置项目文件目录 组织项目文件 打开Visual Studio Start Page 起始页 xff0c 新建项目 New Project xff0c C 43 43 下的General Empty Project
  • 八、C++中的循环 for while do...while及控制流语句

    xff08 1 xff09 for while do while循环 简单来讲 xff0c 循环就是多次执行同样的代码 例 xff0c 如何输出重复内容多次 xff1a 代码复制多次 xff1b 写个函数调用多次 xff1b 写循环重复执行
  • 九、C++ 指针 pointer 引用 Reference

    原始指针 raw pointer 非智能指针 smart pointer 指针是一个整数 xff0c 一个数字 xff0c 它存储一个内存地址 创建指针 xff1a void ptr 61 0 void指针表明不关心指针存储地址的数据类型
  • 基础电路-电子电路

    1 1 电流 xff1a 在导体的两端加上电压 xff0c 导体的电子就会在电场的作用下做定向运动 xff0c 形成电子流 xff0c 称之为 电流 xff1b 电流方向和电子移动方向相反 xff1b 电流有直流和之分 1 2 电压 xff
  • CTP工作原理及结构

    1 工作原理 自容TP xff1a 自电容的扫描方式是进行逐行扫描 xff0c 所有通道既是感应通道也是驱动信号通道 xff0c 通过对比触摸前后电容量的变化来判定X Y方向坐标 xff0c 然后组合成触摸点的坐标 互容TP xff1a 互
  • LCD工作原理及结构

    1 主流显示面板技术 xff1a LCD xff0c OLED xff0c MicroLED 2 主流显示屏的发展趋势 3 LCD堆叠结构 xff1a 背光 xff0c 下偏光片 xff0c TFT Glass xff0c CF Glass
  • OLED工作原理及结构

    1 主流显示面板技术 xff1a LCD xff0c OLED xff0c MicroLED 2 主流显示屏的发展趋势 3 OLED堆叠结构 xff1a 相比LCD xff0c OLED没了背光和下偏光片 4 OLED发光原理 xff1a

随机推荐

  • Git Fork操作与配置

    https blog csdn net Lu Ca article details 128612125 我理解的git fork xff1a 将别人 xff08 张三 xff09 仓库包括文件 xff0c 提交历史 xff0c issues
  • 背光的工作原理及结构

    1 什么是背光 xff1a LCD本身是不发光的 xff0c 因此它需要一个外部面光源系统来帮助其显示 xff0c 即背光源 xff08 Backlight xff09 xff1b 2 背光的种类 xff1a 从发光类型来分 xff0c 可
  • Microled简介及关键工艺(巨量转移)

    1 什么是Microled xff1f 通俗来讲 xff0c 将作为发光器件LED用作显示屏的像素 xff0c 固这种LED的尺寸就得做得很小 xff08 约 xff1c 100 m xff09 xff0c 这种技术就是Microled x
  • 数字电路基础-逻辑门电路

    1 门电路的概念 实现基本逻辑运算和复合运算的单元电路称为门电路 xff0c 常用的门电路有非门 与非门 或非门 与非门 与或非门等 2 基本逻辑门电路 a 与门 xff1a 全1出1 xff0c 有0出0 工作原理 xff1a 输入A B
  • 51单片机-控制LED灯

    目录 1 硬件设计 2 软件设计 2 1 点亮一颗LED灯 2 2 LED闪烁 2 3 LED流水灯 3 知识点 3 1 while函数的应用 3 2 常用数据类型 3 3 延时函数的自动生成 1 硬件设计 通过原理图分析 xff0c LE
  • 51单片机-74HC595移位寄存器

    目录 1 74HC595芯片介绍 1 1 引脚定义 1 2 工作原理 2 实际应用 2 1 控制数码管 2 2 点阵屏显示图形 2 3 点阵屏显示动画 3 知识点 3 1 位变量 xff08 sbit bit xff09 3 2 字模提取
  • 51单片机-独立按键控制LED灯

    目录 1 硬件设计 2 软件设计 2 1 独立按键控制LED闪烁 2 2 独立按键控制LED状态 2 3 独立按键控制LED显示二进制 2 4 独立按键控制LED移位 3 知识点 3 1 if语句的运用 3 2 常用运算符 1 硬件设计 轻
  • 51单片机-控制数码管

    目录 1 硬件设计 1 1 LED数码管介绍 1 2 原理图设计 2 软件设计 2 1 静态数码管显示 2 2 动态数码管显示 2 3 模块化编程 xff08 头文件 xff09 3 知识点 3 1 数组 3 2 子函数 3 3 switc
  • LCD的制作工艺

    目录 1 LCD简介 2 LCD制程 2 1 Array制程 2 1 1 像素电路 2 1 2 制程简介 2 2 Cell制程 2 3 Module组装 3 总结 1 LCD简介 LCD是目前显示领域的重要组成部分 xff0c 可通过如下链
  • 51单片机-LCD1602

    目录 1 硬件设计 1 1 LCD1602介绍 1 2 原理图 2 软件设计 2 1 控制原理 2 2 控制时序 2 3 指令集 2 4 程序设计 3 知识点 3 1 宏定义 3 2 指针与数组 3 3 for语句 1 硬件设计 1 1 L
  • 51单片机-矩阵键盘

    目录 1 硬件设计 2 软件设计 2 1 矩阵键盘扫描 2 2 矩阵键盘 密码锁 3 知识点 3 1 模块化编程的应用 3 2 include详解 1 硬件设计 矩阵键盘按键数量较多 xff0c 为了减少I O口的占用 xff0c 通常将按
  • 如何优雅的让fork后的仓库与原仓库同步

    https www cnblogs com tudou1179006580 p 14875486 html https github com selfteaching the craft of selfteaching issues 67
  • UDP——C语言socket编程

    UDP编程相比TCP来说相对简单主要用到sendto 和recvfrom 这两个函数 xff0c 接下来主要介绍一下这两个函数 头文件 span class token macro property span class token dir
  • 函数与宏定义

    前言 本文介绍函数与宏定义 一 函数的概念 1 函数的定义 自定义函数的形式 xff1a 存储类型符 返回值类型符 函数名 xff08 形参说明表 xff09 函数语句体 xff08 1 xff09 存储类型符 函数作用范围 xff0c 默
  • 51单片机-定时器中断

    目录 1 定时器 计数器 1 1 工作原理 1 2 相关寄存器 1 3 工作模式 xff08 模式1 xff09 2 中断系统 2 1 中断的概念 2 2 中断结构 2 3 中断源 2 4 相关寄存器 3 如何配置定时器中断 4 实际应用
  • 51单片机-串口通信

    目录 1 什么是通信 1 1 通信的概念 1 2 传送方式 1 3 同步方式 1 4 传送方向 1 5 校验方式 2 单片机串口介绍 2 1 硬件电路 2 2 电平标准 2 3 常见接口 2 4 内部结构 2 5 相关寄存器 2 6 工作模
  • I2C总线

    目录 1 硬件概述 1 1 常用器件 1 2 总线结构 1 3 工作原理 2 协议概述 2 1 传输格式 2 2 传输特点 2 3 读和写 2 4 时钟同步和总线仲裁 3 转换器概述 4 缓存器概述 1 硬件概述 1 1 常用器件
  • 蓝牙与WiFi

    蓝牙 透传 HAL UART Receive IT amp huart2 uint8 t amp USART2 NewData 1 开启串口2接收中断 RS485orBT 61 0 RS485orBT标志位为1时是RS485模式 xff0c
  • ubuntu 安装任意版本cmake,无须卸载

    ubuntu1804默认的cmake版本为3 10 2 xff0c 在使用中 xff0c 我们经常会遇到cmake要求更高的版本 xff0c 请读者务必不要轻易卸载原有cmake xff0c 因为这样会把之前用 cmake 编译好的包都给卸
  • 什么是结构体【详解】

    本期介绍 x1f356 主要介绍 xff1a 什么是结构体 xff0c 结构体的声明 定义 初始化 以及传参 xff0c 匿名结构体类型 xff0c 如何通过结构体来实现链表数据结构 xff0c 结构体在内存中是如何存储的 xff08 即