目录
前言:
正文:结构体:
1.结构概述:
2.结构的声明:
3.特殊声明:
4.结构的自引用:
5.结构的定义与初始化:
6.结构体内存对齐(超重点★★★★★):
7.修改默认对齐数:
8.结构体传参:
总结:
️博客主页:✈️努力学习的銮同学
️欢迎关注:点赞收藏✍️留言
️系列专栏:【进阶】C语言学习
家人们更新不易,你们的点赞和关注真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!
关注我,关注我,关注我,你们将会看到更多的优质内容!!!
本文重点 :
结构体声明结构体自引用结构体定义
结构体初始化结构体内存对齐结构体传参
前言:
上文中我详细全面的为各位小伙伴们整理出了在面试中常用的字符串除了函数,并且为大家讲解了每个字符串处理函数的语法结构和使用方法。而在我们的二面笔试中,还有一个非常重要常考的知识块,那就是我们今天的讲解内容——结构体。
正文:结构体:
1.结构概述:
C 语言允许用户自己指定这样一种数据结构,它由不同类型的数据组合成一个整体,以便引用,这些组合在一个整体中的数据是互相联系的,这样的数据结构称为结构体,它相当于其它高级语言中记录。结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
2.结构的声明:
这部分内容在前面初阶结构体已经讲过了,不再做过多的赘述,在此仅以描述 “ 学生 ”为例演示其声明与定义的过程:
#define _CRT_SECURE_NO_WARNINGS 1 #include //结构体的声明: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#includestruct//没有声明结构体标签,即为匿名结构体类型{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 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.结构体内存对齐(超重点★★★★★):
经过上面的学习,我们就已经基本掌握了结构体的使用了。接下来我们将要深入研究结构体大小的计算过程,即结构体内存对齐,而这也是近年来许多公司面试与笔试中的热门考点。
我们先来看看下面这段计算结构体变量大小的代码:
#includestruct 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 | 9 | 10 | 11 | 12 |
内容 | char a |
接着,第二个成员为占据4个字节的 int 类型变量 b,按照规则我们首先要计算它的对齐数,我们将变量 b 的大小4与对齐数默认值8进行比较,得出较小值为4,即对齐数为4,于是我们将它放在对齐数的整数倍处,即最近位置第四字节处:
\ | struct TEST1 | ||||||||||||
偏移量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
内容 | char a | int b |
再接下来是第三个结构体成员占据1个字节的 char 类型变量 c,同样按照规则我们计算出它的对齐数为1,并将它放在对齐数的整数倍处,即最近位置第九字节处:
\ | struct TEST1 | ||||||||||||
偏移量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
内容 | char a | int b | char c |
最后,根据规则,结构体的总大小为最大对齐数的整数倍,而这三个变量中,对齐数最大的是 int 类型变量的对齐数4,则总大小应当为4的倍数。而既为4的倍数,又要能够容纳所有的结构体成员,最小的结构体大小应当为12个字节,即为结构体变量 test1 的大小。
同理各位小伙伴们下去以后可以自己尝试推算结构体变量 test2 的大小并进行验证。
但是我们发现,这样的方式造成了很大程度上的空间浪费,以 test1 为例,12个字节的大小中有六个字节的空间申请了但却没有被使用。那么为什么还要采用这样的办法呢?主要有以下两个原因:
1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
通俗来说结构体的内存对齐就是一种用空间换时间的处理方法。
而我们能做的,就只有以上面的 test1 与 test2 为例,尽可能的选取 test2 这样,使占用空间小的成员尽可能的集中在一起。
7.修改默认对齐数:
在我们的代码编写过程中,默认的对齐数可能会不够合适。而当这个时候,我们就可以通过使用下面这个预处理指令来修改我们的默认对齐数:
#pragma pack(8)//修改默认对齐数为8
我们也可以通过该指令在修改过默认对齐数之后,取消设置的默认对齐数,将其还原:
#pragma pack()//取消设置的默认对齐数,还原为默认
8.结构体传参:
结构体传参与函数传参类似,没有什么疑难点,我们直接来看下面的示例:
#includestruct 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 !!!
天下风云出我辈,一入江湖岁月催。皇图霸业谈笑中,不胜人生一场醉!!!
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们的点赞和关注对我真的很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!