当前位置:首页 » 《我的小黑屋》 » 正文

【C语言】自定义类型(结构体、枚举、联合的详解)

12 人参与  2024年11月06日 09:22  分类 : 《我的小黑屋》  评论

点击全文阅读


写在前面

今天是10月24日来到了一年一度的程序?节,不才在这里就祝各位看官?节日快乐啦~

文章目录

写在前面一、结构体1.1结构的声明:1.2匿名结构体1.3结构体的自引用1.4结构体变量的定义和初始化1.5计算结构体的大小1.6修改默认对齐数1.7结构体传参

一、结构体

结构体是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。

1.1结构的声明:

struct tag {member-list;}variable-list;
struct:是结构体定义标识符tag:是结构体标签member-list:结构体成员变量variable-list:结构体全局变量名

例如创建一个学生结构体:

struct Stu{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号};//分号不能丢
Stu标签了这个结构体是学生结构体结束要使用 ’ ; ',不能缺少在struct Stu中我们没有创建全局结构体变量,所以为空。struct Stu是这个结构体的类型

在声明结构的时候,可以不完全的声明。又称为匿名结构体

1.2匿名结构体

在C语言中,蓝色匿名结构体(也称为无名结构体)是指在定义时不显式指定结构体标签(即struct tag部分)的结构体。匿名结构体通常用于只需要在蓝色局部范围内使用一次的场合,或者当蓝色结构体名称可能会导致命名冲突或不必要的复杂性时。

在这里插入图片描述

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

上面的两个结构在声明的时候省略掉了结构体标签(tag)。只定义了一个结构体全局变量。这时候结构体的类型是未定义的,我们在main函数中也不能主动的定义,只能使用全局变量x来访问匿名结构体。匿名结构体的生命周期和作用域取决于结构体变量的生命周期和作用域。

struct {int a;double b;}my;int main() {my.a = 20;my.b = 10.12;{struct {int n1;double n2;}my_struct;my_struct.n1 = 80;my_struct.n2 = 88.48;printf("%d %.2f\n", my_struct.n1, my_struct.n2);}//printf("%d %.2f", my_struct.n1, my_struct.n2);printf("%d %.2f\n", my.a, my.b);return 0;}
在上面代码中,代码块的my_struct结构体作用域和生命周期都只在代码块中,出了代码块,my_struct结构体就结束生命周期了,如果在代码块外使用my_struct结构体,编译器报错。

运行结果:

匿名结构体虽然没有标签,但是每个匿名结构体的类型都不相同,在编译阶段,编译器会自动帮每个定义匿名结构体标签,每个标签都是不重复的。

举个栗子:

struct {int a;double b;}my;struct {int a;double b;}*p1, tmp;int main() {p1 = &my;return 0;}

警告:编译器会把上面两个声明当作两个完全不相同的类型,所以是非法的。在这里插入图片描述


1.3结构体的自引用

结构体可以包含结构体,但是在结构中包含⼀个类型为该结构本身的成员是不可行的。

struct Node{int data;struct Node next;//err};int main() {struct Node s1 = { 10,{20} };return 0;}

我们在结构体中包含⼀个类型为该结构本身的成员,计算struct Node类型占用空间是无法计算的。如想在结构体中包含一个结构体信息,我们就需要使用结构体指针。

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

其中,我们在结构体包含⼀个类型为该结构本身的成员,必须使用结构体类型来定义,不能使用结构体全局变量或重命名后的新类型名来定义。

typedef struct Node{int data;struct Node* next;//kk1 next1;//err//n1 next2;//err}kk1, n1;

1.4结构体变量的定义和初始化

我们举例说明结构体变量的定义和初始化

struct Node{int data;struct Point p;struct Node* next;} n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化struct Point{int x;int y;} p1;//声明类型的同时定义全局变量p1struct Point p2;//定义结构体全局变量p2int main() {struct Point p3 = { 10, 20};//初始化:定义变量的同时赋初值。return 0;}

我们在声明类型后,如果结构体标签很长的情况下,我们在后续定义该结构体类型就相对麻烦,所以我们可以使用typedef重命名结构体类型。

typedef struct Node{int data;struct Node* next;}kk1, n1;int main() {kk1 s1 = { 100 };struct Node s2 = { 6 };s1.next = &s2;return 0;}
kk1与n1都是struct Node重命名后的新名kk1与n1是与struct Node都是相等的类型。

1.5计算结构体的大小

在结构体中的内存布局是特殊的,涉及到内存对齐。

为什么存在内存对⻬?

⼤部分的参考资料都是如是说的:

平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能 在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。 原因在于,为了访问未对⻬的 内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。

总体来说: 结构体的内存对齐是拿空间来换取时间的做法。

那么如何计算内存对齐?

首先得掌握结构体的对齐规则:

第⼀个成员在与结构体变量偏移量为0的地址处。 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

对齐数 = 编译器默认的⼀个对齐数 与 该成员大小的较小值。

编译器中默认对齐数的值为8

结构体总大小为最大对齐数(每个成员变量都有⼀个对齐数)的整数倍。

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

在这里插入图片描述

struct S1{char c1;int i;char c2;};

计算struct S1中结构体成员的偏移量

首先第一个变量默认存放在偏移量为0的地址处,所以 c1存放在偏移量为0的地址处,自身大小为1个字节,所以c1在内存中的存储如下图在这里插入图片描述

接着就要存放int i; int 类型的偏移量为:自身大小4,默认偏移量为8,取较小值即偏移量为4。所以存放在偏移量为4的倍数上。如下图在这里插入图片描述

接着就要存放char c2; int 类型的偏移量为:自身大小1,默认偏移量为1,取较小值即偏移量为1。所以存放在偏移量为1的倍数上。如下图在这里插入图片描述

根据结构体总⼤⼩为最⼤对⻬数(每个成员变量都有⼀个对齐数)的整数倍。

此时有char类型和int类型,对齐数分别对应着:1和4;那最大对齐数为4。

此时结果体成员在内存中占用了9个字节空间,不满足最大对齐数的整数倍,所以接着往后占用空间直到为4的整数倍,离9最近的4的整数倍为12。在这里插入图片描述

所以struct S1结构体就占用了12个字节空间。

我们为了不浪费空间,我们就应该在定义成员变量时,合理安排定义的顺序
例2:
我们还是使用上面的struct S1相同的成员变量,再来判断这个结构体占用多少字节

struct S2{char c1;char c2;int i;};
首先第一个变量默认存放在偏移量为0的地址处,所以 c1存放在偏移量为0的地址处,自身大小为1个字节,所以c1在内存中的存储如下图在这里插入图片描述接着就要存放char c2; int 类型的偏移量为:自身大小1,默认偏移量为1,取较小值即偏移量为1。所以存放在偏移量为1的倍数上。如下图在这里插入图片描述接着就要存放int i; int 类型的偏移量为:自身大小4,默认偏移量为8,取较小值即偏移量为4。所以存放在偏移量为4的倍数上。如下图在这里插入图片描述此时有char类型和int类型,对齐数分别对应着:1和4;那最大对齐数为4。此时结果体成员在内存中占用了8个字节空间,满足最大对齐数的整数倍,所以结构体S2的大小就为:8。

所以在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,就要做到:

让占用空间小的成员尽量集中在⼀起

在结构体中包含结构体,我们怎么计算结构体大小呢?

#include <stdio.h>struct S1{char n1;int i;char n2;};struct S2{char c1;char c2;struct S1 s1;};int main() {printf("%d", sizeof(struct S2));return 0;}

计算S2结构体占用空间的大小:

首先把S2里面的c1、c2在内存中存放好,如下图在这里插入图片描述

根据规则4:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍 。那么S1的最大对齐数是4,c1就存放在了4的倍数中,此时4对齐数是空的,那就存放在4对齐数中,如下图在这里插入图片描述

紧接着就存放S1的整形变量a,int 类型的偏移量为:自身大小4,默认偏移量为8,取较小值即偏移量为4。所以存放在偏移量为4的倍数上。如下图在这里插入图片描述

之后再存放char n2,如下图在这里插入图片描述

根据规则4可知:结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍,S2中的对齐数有:char类型的1与int类型的4。那么最大对齐数是S1结构体的int类型,所以此时需要往后浪费3个字节空间。如下图在这里插入图片描述

1.6修改默认对齐数

使用#pragma这个预处理指令来修改默认对齐数,但是一般很少会使用修改默认对齐数。
举个栗子:

#include <stdio.h>#pragma pack(1)//设置默认对齐数为8 struct s1 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认
上述代码中我们使用了#pragma pack()把默认对齐数改为了1,这时候S1的成员变量都会以此排序。char类型对齐数是1,默认对齐数为1。取最小值:1int类型对齐数是4,默认对齐数是1。取最小值:1那么s1在内存中的存储图即:在这里插入图片描述s1在内存中占用的字节就是8。

每次修改结束后,我们都需要把对齐数恢复到默认对齐数。这样避免后续使用中,出现意料之外的结果。

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

1.7结构体传参

函数传参的时候, 参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

我们知道,在函数传参中,分为 传值传参和 传址传参。那么在结构体传参中,不才更推荐使用: 传值传参。在函数那篇笔记中,我们知道,如果是 传值传参,那 形参是实参的一份临时拷贝,既然是拷贝,那么就 涉及开辟空间,如果我们结构体成员众多,占用空间大的情况下,我们使用传值传参,就会导致在内存中再开辟一个相同大小的空间,会 造成空间浪费,效率上也比较低下。但是使用 传址传参就不会出现传值的问题,只需要开辟一个指针接收实参的结构体即可。


2024-11-1更新说明:下篇已更新:【C语言】自定义类型(结构体、枚举、联合的详解)下


以上就是本章所有内容。若有勘误请私信不才。万分感激?? 若有帮助不要吝啬点赞哟~~??

ps:表情包来自网络,侵删?


点击全文阅读


本文链接:http://zhangshiyu.com/post/183061.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1