C语言————结构体

          接下来我们来了解C语言中很重要的内容:结构体。虽然到现在我们可以创建常量,变量,数组,但是存储的都是相同类型的数据,如果我们需要写入不同数据类型的信息怎么办,例如常见的身份证上的信息,有身份证号,有地址,有名字,有照片。又比如一个学生的学习,有学号,姓名,年龄,等等。这样的话,如果我们还是以前那样一个数据创建一个的话,岂不是很麻烦,当我们需要将不同数据类型存储在一起的时候这就引出了 结构体。

       大家可以先看一下下面的图片,大概了解结构体长什么样子:

struct Stu
//struct创建结构体的必要前提
//stu结构体名字
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//结尾必须这样写
int main()
{
struct Stu s = { "张三", 20, "男", "20230818001" };//按照结构体成员的顺序初始化
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
return 0;
}

     这样大家大概知道结构体是什么样子了吧。

结构体的创建与初始化

        那接下来我们就正式的开始学习今天的主题吧,首先我们要学习如何创建结构体。但其实结构体的创建是很简单的,首先你需要在你要引用结构体的前面创建(这个肯定都能理解,毕竟要用肯定要有,才能使用)接着就是写出以下的内容:

7a11396ffc7e4893bcad2c978ea60966.png

        我们只需要依照上面的图片一样,先写出结构体的标志struct然后一个{}(注意必须在结尾的括号后添加一个;  表示结构体到这里就结束了),在{}中写入结构体(也就是需要的多种类型)。

       看了上面的内容大家应该知道结构体的创建了吧,接下来我们就学习如何将创建的结构体初始化。

bcab91a8f6cf4c63b2249c31b46ad303.png        这样大家应该了解的差不多了吧,将结构体的第一行抄下来后在后面再写一个名字后就可以结构体赋值了,但是需要注意的是赋值的顺序必须与创建结构体的顺序一样。

结构体可以不完全声明

       学习了上面的知识后,大家应该认为结构体都是这样的了吧,但其实嘞,结构体还有特殊的。大家可以看一下下面的代码,大家认为是否有问题。

struct
{
int a;
char b;
}x;

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

        其实这些代码是没有问题的,开始我们讲了在struct后写的是结构体的名字,但是,大家知道现在名字可以改的吧。结构体有名字那么肯定也可以改吧。所以在结构体的最后的括号后再的话就可以重新命名。以上是结构体不完全声明。上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。虽然都是没有错误的。匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。所以我们写结构体的时候尽量按规定创建结构体,不要炫技。
 

结构体里面包含另外一个结构体

        好了,当我们了解结构体不完全声明后的利害之处后。我们大家想一下结构体可不可以包含另外一个结构体,像数组那样。答案当然是肯定的,在一个结构体中可以包含另外一个结果体,并且可以同时对两个结构体初始化。

cffb150d1a56430c96f5aaad76faffba.png

        这里大家看到了吧,在一个结构体中引入另外一个结构体,首先需要将被包含的结构体写在包含结构体的前面。接着在包含结构体中写入结构体,在写被包含结构体的时候,需要将被包含结构体初始化的名字写出来。不知道大家是否了解了。

      那么接下来的结构体初始化的话,其实与我们平常的结构体初始化是差不多的,我们只需要在要初始化被包含结构体的时候,再写一个{},然后在其中初始化被包含结构体就可以了。

4b0eb6333ad0499580738ce976856482.png

结构体自引用

        好了,我们学习了结构体引用引用结构体,那我们可不可以结构体自己引用自己嘞。这就好比说,自己说自己好帅,自己一会就会捡到100块钱一样。那么结构体可以自引用吗?当然是可以的。

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

      大家觉得这个正确吗?如果正确的话。sizeof(struct Node)结果是什么样的嘞。如果真的去尝试的话,大家会发现,系统会报错的。因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大⼩就会⽆穷的⼤,是不合理的。所以上面的引用是错误的。正确的自引用方法是:

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

       提前学习过的应该知道一个指令typedef(这是一个修改名字的指令)如:

     经过上面的操作后,我们就给int取了一个名字mun了,当然int还是可以继续使用的。那么我们可以将这个指令引入结构体中吗?答案是可以的,虽然可以使用但却很容易出现问题。比如大家可以看一下下面的图片大家思考一下下面的图片是否正确:

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

        大家认为这个代码是正确的吗?大家可以看到我在使用typedef的时候并未对结构体命名,那么这个结构体就是前面说的匿名结构体。Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使⽤Node类型来创建成员变量,所以这是不⾏的。

        那么解决犯法是怎么搞呢?其实我们只需要定义结构体不要使⽤匿名结构体了,那具体在结构体使用typedef的正确方法是什么样子的嘞:

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

结构体大小计算

      我们学习到现在已经掌握了结构体的基本使用了,那我们就进一步,来讨论如何计算结构体的大小。计算结构体大小的话,就不得不了解一个知识点:结构体的内存对齐。那么结构体内存对齐规则大致是下面这四条:

      我们直接举一个例子来分析吧,大家看一下下面的结构体,并且可以猜一下下面的结构体大小是多少?

       大家想到了吗,答案是24,大家可能会想啊,不就是1+8+1=10吗。怎么等于24啊。这可不是我瞎说的啊。大家先看一下代码运行下来是什么样子的

       是吧,结果就是24,那为什么会是24呢?结构体的对齐规则说过在结构体中用结构体成员中最大的当对齐数,如果大于了默认的对齐数的话,结构体对齐数就变成了默认数。我们看上面的代码,最大的是double(8)而默认数是也是8.那么这个对齐数不会改变。那么接下来我们来排一下,因为对齐规则说过,堆放都是从0开始,那么char一个字节,那么占第一个,但是对齐数个字节呀,1,2,3,4,5,6,7都不是8的倍数啊,所以double只有在第8个位置安放才符合规则。然后double本身8个字节那么现在就用了16个字节了,但有7个字节是空的,因为对齐规则跳过了。最后还有一个char,17.18.19.20.21.22.23也不是8的倍数,那么char只有在第24个字节的时候储存,那么现在就是24个字节,但是浪费了14个字节。大家可以看一下下面的图片

    这样大家是否了解了一些了,那我们再举一个例子:

        大家可以思考一下上面的结果是多少?因为这个与我们第一个讲的是差不多的只是换了个位置,但是结果却不一样哈。好了,答案是16。为什么嘞,我在给大家讲一下。1. 第一个是char类型,后面每个数据成员存储的起始位置要从该成员大小的整数倍开始算(也就是1的倍数,故char num2 的起始位置是1,double num3的起始位置是2)。\n2.判断当前总内存(10)是否是最大成员内存(8)的整数倍,如果不是需要补齐到满足最大成员内存的最小整数倍(2倍,16)。\n3.故double num3的起始位置修改为8,char num1,char num2,补齐到8个内存,故结构体所占内存为16。这次大家应该就知道了吧。

好了,我们再讲一个特殊一点的,嵌套结构体如何计算例如:

        规则中将嵌套结构体的对齐数,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。也就是说上面的对齐数是8,。如果s3的最大改为4,那么对齐数也会改变。那么这个代码结果是多少嘞?

       那我们分析一下,1.struct S1的内存为16(char b,int c一起补齐8个,共补3个)。\n2.S2中嵌套结构体S1(结构体成员要从其内部最大元素大小的整数倍地址开始存储),当前S1中最大元素大小为8,S2中double也是8,故S2最大对齐数是8的倍数\n3. char num补齐到8,结构体的总内存为(8+16+8=32)。好了那么大家应该知道如何计算结构体大小了吧

 为什么存在内存对齐?

1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。反正呀,我个人认为知道有这个事情就可以了,大佬的事情我们不知道我们也不敢问。

 修改对齐数

#pragma pack,这个预处理指令,可以改变编译器的默认对⻬数。那我们也不废话直接上代码

     在代码的前面#pragma pack(),括号里面就是修改的对齐数。并且我们都知道做事要有始有终,我们既然修改对齐数,那么我们后面肯定要将对齐数改回来呀,所以我们在结构体末尾写上#pragma pack(),那么对齐数就变回去了。

结构体传参

      我们创建了结构体,使用肯定不能像开头那样直接打印一下,我们可以将结构体内容传出去,就是传参嘛。大家可以看一下下面的代码,虽然结构是一样的,但是大家认为哪一种更好一些嘞?

       肯定是选着第二个代码,为什么嘞?因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

总结:结构体传参的时候,要传结构体的地址。


位段

大家应该很少听过这个词汇吧。

struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};

上面的A就是⼀个位段类型。,那么位段是定义是什么嘞

我们如何计算位段大小,大家可以看到上面代码后面都跟着一个数字,那个就是给成员分配的比特位,比如说a就分配了5个比特位,b就是5,c是10,d是30。

位段的内存分配

1.位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

 位段的跨平台问题


1. int位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会
出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃
剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

位段的应⽤


下图是⽹络协议中,
IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

位段使⽤的注意事项


位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

struct A
{
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}

       好了,以上就是鄙人想与大家分享的关于结构体的知识了,肯定还有很多不足甚至错误的地方,希望大家可以在评论区中写出来,以便改正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/414636.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

进程间通信,无名管道pipe,有名管道mkfifo,信号的基础概念,signal函数

我要成为嵌入式高手之2月28日Linux高编第十一天&#xff01;&#xff01; 学习笔记 进程间通信 总共6种通信方法&#xff0c;主要是前五种方式 第一种方式&#xff1a;管道 一、无名管道 只能用于具有亲缘关系的进程间通信 pipe #include <unistd.h> int pipe(i…

ETH开源PPO算法学习

前言 项目地址&#xff1a;https://github.com/leggedrobotics/rsl_rl 项目简介&#xff1a;快速简单的强化学习算法实现&#xff0c;设计为完全在 GPU 上运行。这段代码是 NVIDIA Isaac GYM 提供的 rl-pytorch 的进化版。 下载源码&#xff0c;查看目录&#xff0c;整个项目…

2024年3月5-7日第12届生物发酵技术装备展-奥博仪表

参观企业介绍 潍坊奥博仪表科技发展有限公司成立于2002年3月&#xff0c;注册资金1000万元&#xff0c;已有20多年的发展历程&#xff0c;是一家专业从事流量仪表开发、生产与测控系统集成的高新技术企业和双软认证企业。 目前公司以仪表、通讯产品、自控系统、软件的研发、生…

DVWA 靶场之 Command Injection(命令执行)middlehigh

对于 middle 难度的 我们直接先看源码 <?phpif( isset( $_POST[ Submit ] ) ) {// Get input$target $_REQUEST[ ip ];// Set blacklist$substitutions array(&& > ,; > ,);// Remove any of the characters in the array (blacklist).$target str_rep…

Pytest教程:一种利用 Python Pytest Hook 机制的软件自动化测试网络数据抓包方法

随着计算机技术的发展&#xff0c;使得网络应用的数量不断增加&#xff0c;因此网络数据抓包成为了网络应用开发和测试中非常重要的一部分。目前&#xff0c;已有许多网络数据抓包工具可供使用&#xff0c;例如 Wireshark、Tcpdump、Fiddler 等&#xff0c;但这些工具需要手动配…

5G网络频谱划分与应用

频率越大&#xff0c;波长越短。补充&#xff1a;微波频段&#xff1a;300MHZ~3000GHZ。 5G网络工作频带与带宽配置 &#xff08;1&#xff09; FR1:410MHZ~7125MHZ。 FR2&#xff1a;24250MHZ~52600MHZ 注&#xff1a;早期将6GHZ已下频段作为FR1&#xff0c;但是最新的频段…

Python打发无聊时光:10.用flask创造按键控制的网页小游戏

游戏介绍: 《秦蓝大冒险》是一款简单而紧张的追逐游戏。在这个游戏中&#xff0c;玩家将控制一名名叫“吕千”的角色&#xff0c;试图在一个封闭的空间内逃避一个名为“秦蓝”的追逐者。随着时间的推移&#xff0c;“秦蓝”会不断追踪玩家的位置&#xff0c;努力捕捉到他。 游…

C语言中如何进行内存管理

主页&#xff1a;17_Kevin-CSDN博客 收录专栏&#xff1a;《C语言》 C语言是一种强大而灵活的编程语言&#xff0c;但与其他高级语言不同&#xff0c;它要求程序员自己负责内存的管理。正确的内存管理对于程序的性能和稳定性至关重要。 一、引言 C 语言是一门广泛使用的编程语…

2.1_6 线程的实现方式和多线程模型

文章目录 2.1_6 线程的实现方式和多线程模型&#xff08;一&#xff09;线程的实现方式&#xff08;1&#xff09;用户级线程&#xff08;2&#xff09;内核级线程 &#xff08;二&#xff09;多线程模型&#xff08;1&#xff09;一对一模型&#xff08;2&#xff09;多对一模…

stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程

stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程 AI浪潮袭来&#xff0c;还是学习学习为妙赛博菩萨【秋葉】简介——&#xff08;葉ye&#xff0c;四声&#xff0c;同叶&#xff09;A绘世启动器.exe&#xff08;sd-webui-aki-v4.6.x&#xff09;工具包安…

VirtualBox虚拟机配置双网卡

安装完后Linux后。下一步需要设置Linux的网络。为了便于学习测试&#xff0c;通常我们需要虚拟机能通宿主机和外网。类似达到下面的效果。虚拟机跟宿主本机和外网&#xff0c;及另外一台同网段的虚拟机也是相通 解决思路是-->配置双网卡&#xff0c;网卡1使用NAT网络模式&a…

vue组件中data为什么必须是一个函数

查看本专栏目录 关于作者 还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#x…

fiddler抓包工具使用(一)

一、fiddler简介 1. 简介 fiddler是一款强大的抓包工具&#xff0c;它的原理以web代理服务器的形式进行工作fiddler是好用的web调试工具之一 能记录所有客户端和服务器的http和https请求修改输入、输出数据包数据允许监视设置断点弱网测试 2. 工作原理 代理就是在客户端和服…

QT C++实战:实现用户登录页面及多个界面跳转

主要思路 一个登录界面&#xff0c;以管理员Or普通用户登录管理员&#xff1a;一个管理员的操作界面&#xff0c;可以把数据录入到数据库中。有返回登陆按钮&#xff0c;可以选择重新登陆&#xff08;管理员Or普通用户普通用户&#xff1a;一个主界面&#xff0c;负责展示视频…

java动态代理面试题,java反射原理面试题

01 并发宝典&#xff1a;面试专题 面试专题分为四个部分&#xff0c;分别如下 Synchronized 相关问题 可重入锁 ReentrantLock 及其他显式锁相关问题 Java 线程池相关问题 Java 内存模型相关问题 1.1 Synchronized 相关问题&#xff08;这里整理了八问&#xff09; 问题一…

揭示预处理中的秘密!(二)

目录 ​编辑 1. #运算符 2. ##运算符 3. 命名约定 4. #undef 5. 命令行定义 6. 条件编译 7. 头文件的被包含的方式 8.嵌套文件包含 9. 其他预处理指令 10. 完结散花 悟已往之不谏&#xff0c;知来者犹可追 …

【Go-Zero】测试API查询信息无法返回数据库信息与api、rpc文件编写规范

【Go-Zero】测试API查询信息无法返回数据库信息与api、rpc文件编写规范 大家好 我是寸铁&#x1f44a; 总结了一篇测试API查询信息无法返回数据库信息与api、rpc文件编写规范的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 问题背景 大家好&#xff0c;我是寸铁&#xff01…

C++——基础语法(2):函数重载、引用

4. 函数重载 函数重载就是同一个函数名可以重复被定义&#xff0c;即允许定义相同函数名的函数。但是相同名字的函数怎么在使用的时候进行区分呢&#xff1f;所以同一个函数名的函数之间肯定是要存在不同点的&#xff0c;除了函数名外&#xff0c;还有返回类型和参数两部分可以…

前后端项目-part03

文章目录 5.4.4 机构名称5.4.4.1 创建实体类Company5.4.4.2 创建实体类CompanyMapper5.4.4.3 创建实体类CompanyService5.4.4.4 创建实体类CompanyController5.4.4.5 后端测试5.4.4.6 修改basic.js5.4.4.7 修改course.vue5.4.4.8 测试5.4.5 课程标签5.4.5.1 效果5.4.5.2 修改co…

golang学习5,glang的web的restful接口

1. //返回json r.GET("/getJson", controller.GetUserInfo) package mainimport (/*"net/http"*/"gin/src/main/controller""github.com/gin-gonic/gin" )func main() {r : gin.Default()r.GET("/get", func(ctx *…