🚀前言
在C语言中有着各种数据类型,这些类型有配划分为内置类型和自定义类型两大类(如下图)。铁子们,今天阿辉要分享的就是自定义类型中的结构体,联合体和枚举将在下篇文章分享,至于数组阿辉之前的文章数组篇中已经详细讲到,铁子们感兴趣的话可以点击跳转😘,不多bb直接开始我们今天的学习👊
🚀结构体
铁子们是否有这样的疑问——C语言为什么要引入结构体这一自定义类型?
别急,听阿辉一一道来👇
其实结构体与数组有一点类似,数组是存储同一种数据类型的集合,而结构体是存储不同类型的集合,比如当你想描述一个学生时,你得有姓名、年龄、学号等等一系列特征
可是我们发现这不是某一种单一的数据类型能够描述的,这时引入结构体这一自定义类型是非常有必要的
对于结构体有何用想必铁子们有了初步的认识,咱们接着往下看👇
✈️结构体类型的声明
声明结构体的语法结构:
struct tag { member_list; 成员列表, }variable_list; 变量列表,在结构体声明时就创建的变量 struct tag 这个整体属于类型名,和int,char等等类型名一样
我们来创建一个描述学生的结构体类型:
struct stu { char name[20];名字 int age;年龄 int id;学号 char sex[5];性别 };//注意这里的分号不能丢了
结构体类型的声明同样分为全局声明和局部声明,结构体全局声明以及声明时创建的变量作用域都是整个程序,而结构体的局部声明以及声明时创建的变量的作用域在该大括号内部{}
✈️结构体变量的创建与初始化
结构体变量有两种创建方式,一种在结构体类型声明时就创建,与结构体类型声明具有相同的作用域;另一种在结构体类型声明后创建,作用域与结构体类型声明无关,咱直接上代码👇
struct stu { char name[20]; int age; }s1;//s1属于全局变量 int main() { struct stu s2;//s2局部变量,作用域在main函数内 return 0; }
结构体变量初始化:
#include <stdio.h> struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; int main() { //按照结构体成员的顺序初始化 struct Stu s1 = { "张三", 20, "男", "20230818001" }; //按照指定的顺序初始化 struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥}; return 0; }
✈️结构体类型的特殊声明
铁子们,结构体还有一种特殊的声明,这种声明把结构体的标签tag
给干掉了,这种特殊声明的结构体被称为匿名结构体类型
我们来上一组例子:
struct { int a; int b; }a; struct { int a; int b; }*p; int main() { p = &a; return 0; }
上⾯的两个结构在声明的时候省略掉了结构体标签tag
那么问题来了?
p = &a
这样写是否合法?
对于匿名结构体类型,上述两个结构体类型看似一样,实则不同,匿名结构体的变量只能在声明时创建且只能有一个变量,上述编译器会把匿名结构体指针变量p与&a当作两个不同的类型
✈️结构体的自引用
结构体的自引用本质是结构体的递归定义,但是这会存在很大问题,如下面这个代码
struct node { int data; struct node next; }
在编译期间,编译器需要知道结构体变量大小为结构体变量分配空间,但是上述这个结构体我们仔细想一下会发现这个结构体无限递归,根本无法确定其大小,为解决上述问题,我们可以通过指针来间接引用结构体,如下:
struct node { int data; struct node* next; }
上述就是正确的结构体自引用,通过结构体自引用我们可以创建具有互相关联关系的数据结构,如链表、树等,在数据结构中结构体尤为重要。
✈️结构体的内存对齐
有了上面对于结构体的理解,铁子们对结构体的基本使用应该不成问题了,接下来咱们来研究一个深入的问题——结构体类型的大小
有的老铁可能会说:不是很简单吗❓直接把所有变量所占字节空间大小全都加起来就完事了
但是真有这么简单吗?我们接着看👇
其实结构体存在内存对齐这一规则:
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
VS中默认的值为8,Linux中gcc没有默认对齐数对齐数就是该成员大小- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
在VS中char
的对齐数就是1
,int
对齐数就是4
,double
对齐数就是8
我们来看看一个是否如此
#include <stdio.h> struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct S1)); return 0; }
我们可以看到对于
两个char和一个int
应该是6
个字节,但是通过sizeof
却打印出了12
这里我用用图为铁子们解释:
上图一个方块代表一个字节,对于第一个成员c1
就存在偏移量为0的地址处,而对于第二个成员i
它是int
类型对齐数为4
,要存在为4
的倍数的偏移量处也就是上图位置出,第三个成员c2
为char
类型对齐数为1
存在i
成员后面,现在整个大小只有9个字节并非最大对齐数4
的整数倍所以还要补3
个字节分配给它,上图中蓝色方块代表浪费的内存
知道了结构体内存对齐的计算之后,问题又来了:为什么存在内存对齐❓
有两个原因:
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
结构体的内存对齐是一种拿空间换时间的做法
在设计结构体的时候,我们既要满足对齐,又要节省空间,这该如何做呢?
在我们声明结构体时尽量让占用空间小的成员集中在一起,如:
#include <stdio.h> //例如: struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
struct s2
就要比struct s1
的空间小4
个字节
🚁修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数
例子:
#pragma pack(1)//设置默认对齐数为1 struct S1 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S1)); return 0; }
输出为6
当默认对齐数为1
时也就不存在对齐了,直接把所有变量所占字节空间大小全都加起来就完事了
✈️结构体传参
与其他类型变量传参一样,同样可以传址和传值
#include <stdio.h> struct S { int data[1000]; int num; }; struct S s = { {1,2,3,4}, 1000 }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s); //传结构体 print2(&s); //传地址 return 0; }
上述代码都可以帮我们打印,但是传址调用更好
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
SO结构体在传参时要传递结构体地址
感谢老铁能看到这,到这里结构体的分享就到此为止了,如果觉得阿辉写得不错的话,记得给个赞呗,你们的支持是我创作的最大动力🌹