草庐IT

【C语言进阶】参加面试怎能不会结构体?进来学,手把手教会你结构体的原理与使用

銮崽的干货分享基地 2024-05-09 原文

目录

🤩前言🤩:

🤯正文:结构体🤯:

        1.结构概述🍗:

        2.结构的声明🍔:

        3.特殊声明🍟:

        4.结构的自引用🍣:

        5.结构的定义与初始化🍱:

        6.结构体内存对齐(超重点★★★★★)🧆:

        7.修改默认对齐数🥗:

        8.结构体传参🌮:

🥳总结🥳:


🛰️博客主页:✈️努力学习的銮同学

🛰️欢迎关注:👍点赞🙌收藏✍️留言

🛰️系列专栏:💐【进阶】C语言学习

        家人们更新不易,你们的👍点赞👍和👉关注👈真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!

        关注我,关注我,关注我,你们将会看到更多的优质内容!!!


🏡🏡本文重点 🏡🏡:

🚅结构体声明🚃结构体自引用🚃结构体定义🚏🚏

🚅结构体初始化🚃结构体内存对齐🚃结构体传参🚏🚏

🤩前言🤩:

        上文中我详细全面的为各位小伙伴们整理出了在面试中常用的字符串除了函数,并且为大家讲解了每个字符串处理函数的语法结构和使用方法。而在我们的二面笔试中,还有一个非常重要常考的知识块,那就是我们今天的讲解内容——结构体

🤯正文:结构体🤯:

        1.结构概述🍗:

        C 语言允许用户自己指定这样一种数据结构,它由不同类型的数据组合成一个整体,以便引用,这些组合在一个整体中的数据是互相联系的,这样的数据结构称为结构体,它相当于其它高级语言中记录。结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量

        2.结构的声明🍔:

        这部分内容在前面初阶结构体已经讲过了,不再做过多的赘述,在此仅以描述 “ 学生 ”为例演示其声明与定义的过程

#define _CRT_SECURE_NO_WARNINGS 1
 
#include<stdio.h>
 
//结构体的声明:
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;
}

        3.特殊声明🍟:

        今天我们关于声明部分要补充的,是关于结构体的不完全声明,即匿名结构体类型

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>

struct
//没有声明结构体标签,即为匿名结构体类型
{
	char name[20];
	int age;
	char sex[5];
	float score;
}student = { "Zhang",21,"Man",91.7 };
//匿名结构体类型必须在生声明的同时进行定义

int main()
{
	printf("%s %d %s %.1f\n", student.name, student.age, student.sex, student.score);

	return 0;
}

        我们把这种在声明时省略掉结构体标签的结构体称为匿名结构体类型,在使用这种方式进行声明时,由于没有声明结构体标签,导致一旦该结构体结束声明,将无法再次进行定义,所以对于该类型的结构体来说,就必须在声明结构体的同时进行定义(可以不初始化)。

        之所以在这里强调这个知识点是因为,在进行完全声明时,例如上面我们的 “ 学生 ”的示例中的s1~s6这六个结构体变量因为声明时声明了结构体标签,所以会被视为同一种类型进行处理和调用

        而我们再来看下面这个例子:

//结构体类型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; 
//一种类型的指针指向另一种不同类型,将被视为非法

        4.结构的自引用🍣:

        顾名思义,结构的自引用就是指结构体在自己的声明中引用了自己的一种声明方式。那么我们来看看下面这段代码,判断一下这样的下面这样的引用方式是否正确:

struct Test
{
	int data;
	struct Test n;
};

int main()
{
	struct Test n;

	return 0;
}

        我们说这种引用方式是非法的。这是因为,当我们这样进行引用后,在我们定义结构体变量时,会进行自引用,但在自引用中又嵌套了对自身的引用,如此循环往复,而编译器并不知道该在何时停止自引用

        正确的自引用形式应当是下面这样的形式:

struct Test
{
	int data;
	struct Test* NEXT;
    //使用指针指向确定的引用空间
};

int main()
{
	struct Test n;

	return 0;
}

        当我们在进行结构体变量的定义时同样进行了自引用,不同的是这一次我们使用了一个指针,指向了下一个结构体变量的空间,而在这次指向之后,指针指向的空间被固定,不再指向其它空间,如此就实现了真正的结构体自引用。

        同时,我们还可以结合关键字 typedef 进行使用

typedef struct Test
{
	int data;
	struct Test* NEXT;
	//但在这里必须仍使用struct Test
	//在结构体声明结束后才会进行重命名
}Test;
//使用tepydef关键字,将struct Test类型重命名为Test类型

int main()
{
	Test n;
	//经过重命名,在进行定义时可以直接使用重命名后的类型名进行定义

	return 0;
}

        我们可以结合关键字 typedef 来将我们声明的结构体变量进行重命名,方便我们对结构体变量定义与初始化。但要注意的是,在使用 typedef 时,在结构体声明内部进行自引用时,仍需写成完全形式,这是因为,只有在结构体声明结束后才会对我们声明的结构体类型进行重命名

        5.结构的定义与初始化🍱:

         这部分内容在之前的初阶结构体(点击跳转)的学习中也已经为大家进行过详细的讲解,且这个部分没有需要额外补充的知识点,这里也就不再做过多的赘述,仅以示例来演示相关的使用:

#define _CRT_SECURE_NO_WARNINGS 1
 
#include<stdio.h>
 
struct student
{
	char name[20];
	int age;
	char sex[5];
	float score;
 
}s1 = { "Zhang",21,"Man",98.4 };
//初始化结构体变量s1,此处的结构体变量是全局的
 
struct student s2 = { "Wang",20,"Woman",99.5 };
//初始化结构体变量s2,此处初始化的结构体变量等同于声明时初始化,也是全局的
 
int main()
{
	struct student s3 = { "Jiao",21,"Man",67.2 };
	//初始化结构体变量s3,此处的结构体变量是局部的
 
	printf("%s %d %s %.1lf\n", s1.name, s1.age, s1.sex, s1.score);
	printf("%s %d %s %.1lf\n", s2.name, s2.age, s2.sex, s2.score);
	printf("%s %d %s %.1lf\n", s3.name, s3.age, s3.sex, s3.score);
 
	return 0;
}

        6.结构体内存对齐(超重点★★★★★)🧆:

        经过上面的学习,我们就已经基本掌握了结构体的使用了。接下来我们将要深入研究结构体大小的计算过程,即结构体内存对齐,而这也是近年来许多公司面试与笔试中的热门考点

        我们先来看看下面这段计算结构体变量大小的代码:

#include<stdio.h>

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

struct test2
{
	char d;
	char e;
	int f;
}test2;

int main()
{
	printf("The size of test1 is %d\n", sizeof(test1));
	printf("The size of test2 is %d\n", sizeof(test2));

	return 0;
}

        各位小伙伴们认为这段代码的计算结果是什么样的呢?

        小伙伴们可能会猜想,TEST1 与 TEST2 两结构体类型中的成员,均是两个占据1个字节的 char 类型变量与一个占据4个字节的 int 类型变量,所以结构体变量 test1 与 test2 的大小应当均为6个字节

        那么小伙伴们的猜想正确吗?我们将其编译运行起来看看结果:

        我们看到,实际的计算结果与我们的猜想大相径庭,那么到底是哪里出现了问题呢?这就是我们在这里需要研究的内容:结构体内存对齐。

        要想弄清楚究竟是如何进行结构体变量大小计算的,我们首先得掌握结构体的对齐规则

1. 第一个成员在与结构体变量偏移量为0的地址处。(偏移量:该成员的存放地址与结构体空间起始地址之间的距离

2. 其他成员变量要对齐到对齐数的整数倍的地址处。

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

4. 对齐数在VS中的默认值为8

5. 结构体总大小为最大对齐数的整数倍

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

        知晓了结构体的对齐规则,我们再回过头来分析上面的结构体变量大小计算过程。

        在结构体变量 test1 中,第一个成员为占据一个字节的 char 类型变量 a,我们按照规则将其放置在偏移量为0,即结构体空间的起始位置

\struct TEST1
偏移量0 1 2 3 4 5 6 7 8 9101112
内容char a

        接着,第二个成员为占据4个字节的 int 类型变量 b,按照规则我们首先要计算它的对齐数,我们将变量 b 的大小4与对齐数默认值8进行比较,得出较小值为4,即对齐数为4,于是我们将它放在对齐数的整数倍处,即最近位置第四字节处

\struct TEST1
偏移量0 1 2 3 4 5 6 7 8 9101112
内容char aint b

        再接下来是第三个结构体成员占据1个字节的 char 类型变量 c,同样按照规则我们计算出它的对齐数为1,并将它放在对齐数的整数倍处,即最近位置第九字节处

\struct TEST1
偏移量0 1 2 3 4 5 6 7 8 9101112
内容char aint bchar c

        最后,根据规则,结构体的总大小为最大对齐数的整数倍,而这三个变量中,对齐数最大的是 int 类型变量的对齐数4,则总大小应当为4的倍数。而既为4的倍数,又要能够容纳所有的结构体成员最小的结构体大小应当为12个字节,即为结构体变量 test1 的大小

        同理各位小伙伴们下去以后可以自己尝试推算结构体变量 test2 的大小并进行验证。

        但是我们发现,这样的方式造成了很大程度上的空间浪费,以 test1 为例,12个字节的大小中有六个字节的空间申请了但却没有被使用。那么为什么还要采用这样的办法呢?主要有以下两个原因:

1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

        通俗来说结构体的内存对齐就是一种用空间换时间的处理方法。

        而我们能做的,就只有以上面的 test1 与 test2 为例,尽可能的选取 test2 这样,使占用空间小的成员尽可能的集中在一起

        7.修改默认对齐数🥗:

        在我们的代码编写过程中,默认的对齐数可能会不够合适。而当这个时候,我们就可以通过使用下面这个预处理指令修改我们的默认对齐数

#pragma pack(8)
//修改默认对齐数为8

        我们也可以通过该指令在修改过默认对齐数之后,取消设置的默认对齐数,将其还原:

#pragma pack()
//取消设置的默认对齐数,还原为默认

        8.结构体传参🌮:

        结构体传参与函数传参类似,没有什么疑难点,我们直接来看下面的示例:

#include<stdio.h>

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

struct TEST test = { {1,2,3,4}, 1000 };

//结构体传参
void Print1(struct TEST test)
{
	printf("%d\n", test.num);
}

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

int main()
{
	Print1(test);  //传结构体
	Print2(&test); //传地址

	return 0;
}

        而在上面这段代码中,我们一般认为 Print2 函数更为优秀。原因是当函数传参的时候,参数是需要压栈的,在这个过程中就会产生时间和空间上的系统开销。如果传递一个结构体对象时结构体过大,那么将会导致参数压栈的的系统开销较大,最终将会导致程序性能的下降

🥳总结🥳:

        通过今天的学习,相信各位优秀的小伙伴们一定可以熟练的掌握结构体的相关原理与使用,并在将来的面试与笔试中一鸣惊人,让前去面试你的面试官眼前一亮,一眼就从人群中相中你们,为你们提供一份让你们满意的 offer !!!

        🔥🔥天下风云出我辈,一入江湖岁月催。皇图霸业谈笑中,不胜人生一场醉!!!🔥🔥

        更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~  你们的点赞和关注对我真的很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

有关【C语言进阶】参加面试怎能不会结构体?进来学,手把手教会你结构体的原理与使用的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  3. ruby - Highline 询问方法不会使用同一行 - 2

    设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案

  4. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  5. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  6. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  7. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  8. 7个大一C语言必学的程序 / C语言经典代码大全 - 2

    嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来

  9. ruby-on-rails - 一般建议和推荐的文件夹结构 - Sinatra - 2

    您将如何构建一个简单的Sinatra应用程序?我正在制作,我希望该应用具有以下功能:“应用程序”更像是一个包含所有信息的管理仪表板。然后另一个应用程序将通过REST访问信息。我还没有创建仪表板,只是从数据库中获取东西session和身份验证(尚未实现)您可以上传图片,其他应用可以显示这些图片我已经使用RSpec创建了一个测试文件通过Prawn生成报告目前的设置是这样的:app.rbtest_app.rb因为我实际上只有应用程序和测试文件。到目前为止,我已经将Datamapper用于ORM,将SQLite用于数据库。这是我的第一个Ruby/Sinatra项目,所以欢迎任何和所有建议-我应

  10. ruby-on-rails - 使用 javascript 更改数据方法不会更改 ajax 调用用户的什么方法? - 2

    我遇到了一个非常奇怪的问题,我很难解决。在我看来,我有一个与data-remote="true"和data-method="delete"的链接。当我单击该链接时,我可以看到对我的Rails服务器的DELETE请求。返回的JS代码会更改此链接的属性,其中包括href和data-method。再次单击此链接后,我的服务器收到了对新href的请求,但使用的是旧的data-method,即使我已将其从DELETE到POST(它仍然发送一个DELETE请求)。但是,如果我刷新页面,HTML与"new"HTML相同(随返回的JS发生变化),但它实际上发送了正确的请求类型。这就是这个问题令我困惑的

随机推荐