【C语言】结构体超详细全讲解 (代码+万字文字+画图讲解)

dcebcb7976884652a4fdad7708b196ea.png

目录

1.什么是结构体类型

2.结构体变量的创建

 3.结构体变量的初始化

4.结构体的特殊声明

5.typedef重定义结构体变量(两种方式)

 6.结构体自引用

 7.创建结构体指针变量

8.结构体内容的访问

8.1直接访问:

8.2 结构体指针访问

9.结构体内存存储 

9.1 结构体内存存储4条规则

例题1&&讲解:

结构体内容的储存方式:

结构体字节的大小:

 例题2&&讲解:

结构体内容的储存方式:

结构体字节的大小:

例题3&&讲解:

 结构体内容的储存方式:

结构体字节的大小:

例题4&&讲解(结构体嵌套问题 )

 结构体内容的储存方式: 

结构体字节的大小:

 10.为什么存在内存对齐?

 11.如何节省结构体内存大小

 12.修改默认对齐数

 13.scand初始化结构体变量

 错误写法如下​编辑:

14.结构体传参

​编辑


小心!VS2022不可直接接触,否则!没这个必要,方源面色淡然一把抓住!顷刻炼化! 


 

1.什么是结构体类型

什么是结构体类型,这里拿一个常见的格式举例:

struct stu  //struct stu 为结构体类型
{
    int a; //结构体内容
}s1;    //s1 为结构体变量

 这是一个很常见的结构体格式

struct stu 为结构体类型

s1 为结构体的变量

int a 为结构体变量的内容


2.结构体变量的创建

结构体变量的创建有两种方法,一种是随着结构体的创建而创建

另一种则是单独创建,示例如下:

方法一:

struct stu1{
    int a;
}s1;    //创建结构体变量s1

 方法二(含注意事项):

struct stu{
int a;
};  //注意这里的 ; 一定要写,不写就是错误的结构体

struct stu s1;    //创建结构体变量s1

 这里采用的是 用结构体类型创建结构体变量

不理解的可以类比int a,类型加变量名


 3.结构体变量的初始化

 结构体变量的初始化可以分为三种情况:


情况1&&情况3:
    变量随着结构体而定义
    定义结构体变量需要采用,变量名.结构体内容 进行初始化结构体内容

情况1:
	变量随着结构体而定义
	定义结构体变量需要采用,变量名.结构体内容 进行初始化结构体内容


示例如下
(情况一):
struct stu  //结构体类型为struct stu
{
	int a;
	char arr[20];
	int d;
}s1, s2;   //结构体变量为s1,s2

//结构体初始化:
s1.a = 20;//相等于直接访问结构体内容,并直接赋值
s1.arr[0] = 'w'; //对单个字符型数组存储'w'
printf("%c \n",s1.arr[0]);//打印字符w
printf("%d ",s1.a);//打印20
------------------------------------------------------
------------------------------------------------------
(情况三)直接将变量初始化
struct stu  //结构体类型为struct stu
{
	int a;
	char arr[20];
	int d;
}s1 = {20,"w",987};    //从数组起始地址开始存储"w"
//直接将变量初始化

 情况2:
变量不随着结构体而定义
定义结构体变量采用, 结构体类型+变量名 的形式进行初始化结构体内容

情况2:
变量不随着结构体而定义
定义结构体变量采用, 结构体类型+变量名 的形式进行初始化结构体内容

示例如下:情况2
struct stu  //结构体类型为struct stu
{
	int a;
	char arr[20];
	int d;
};
struct stu s1 = { 20,"w",987 }; //按照顺序初始化结构体内容
struct stu s2 = { .a = 90,.d = 98799,.arr[0] = 'd' };
//指定顺序初始化,采用 .结构体内容的形式 初始化

printf("%d %c %d\n", s1.a, s1.arr[0], s1.d);
//打印20 w 987
printf("%d %c %d\n", s2.a, s2.arr[0], s2.d);
//打印90 d 98799

4.结构体的特殊声明

如果结构体声明不完全,创建的结构体只能使用一次,示例如下:

struct {
//结构体未完全声明
	int a;
	int b;
}s1;
s1.a = 20;
s1.b = 30;
//未声明的结构体属于匿名结构体类型,只能使用一次

s2.a = 40;
//使用完变量s1后再想创建新的变量s2
//但是因为是匿名结构体类型,所以系统会报错,不能再次创建和使用新的变量

5.typedef重定义结构体变量(两种方式)

 第一种方式:

typedef struct stu node;//将结构体类型 struct stu 重定义为 node
   	struct stu 
    {
		int a;
	};
	node s1 = { 20 };  //这里的node就相当于struct stu
	printf("%d ", s1.a); //这里是结构体的直接访问,变量名.结构体内容,可以直接访问到结构体内容

	//创建结构体指针
	node * p1 = &s1;    //后面讲

第二种方式:

typedef struct stu 
{
	int a;
}node;  //这里不是创建结构体变量node,而是将struct stu重定义为node
node s1 = { 20 };    //这里的node就相当于 struct stu
printf("%d ", s1.a); //直接访问结构体变量s1中的a,打印20


 6.结构体自引用

结构体自引用是一件很可怕的事,结构体最怕的就是重命名,一旦重命名就会出现错误

比如说结构体的变量与结构体的内容重名就会出现错误

那么什么是结构体的自引用呢?结构体的自引用就是一个结构体嵌套自己的结构体

示例如下:

struct stu 
{
	int a;
	struct stu s1;
	//死循环,变量s1里还有结构体,又会创建新的变量s1,空间无限大
};

 使用自己的结构体嵌套自己,就会出现无数多个自己的结构体,结构体的内存就会无限大,是不合理的


 7.创建结构体指针变量

对于结构体来说,结构体在未创建变量时不占用内存

所以结构体的变量名会表示整个结构体,因此&结构体变量名,就相当于取出了整个结构体变量的内存

那么如何创建结构体指针变量呢?示例如下:

 创建结构体指针变量第一种写法:

struct stu {
	int a;//创建结构体指针变量p1,指针变量p1的类型为struct stu *
}s1; 

//创建结构体指针变量 p1,存放结构体变量s1的地址
struct stu* p1 = &s1;
//结构体指针变量p1的类型是struct stu*

 相信有的同志会写出这样的代码:

struct stu {
	int a;//创建结构体指针变量p1,指针变量p1的类型为struct stu *
}s1; struct stu * p1 = &s1;
printf("%p\n", &s1);//打印012FFA30
printf("%p\n", p1); //打印012FFA30

其实这样写也是对的,因为在结构体大括号外的 ; 写完之后,再写句子就是单独的表达式了,所以其实和第一种写法是完全相同的,只是代码放的位置不一样

创建结构体指针变量第二种写法:

struct stu {
	int a;//创建结构体指针变量p1,指针变量p1的类型为struct stu *
}*p; 


8.结构体内容的访问

结构体内容的访问可分为两种,一种为直接访问,另一种为通过结构体指针访问

8.1直接访问:

struct stu  //结构体类型为struct stu
{
	int a;
	char arr[20];
	int d;
}s1 = { 20,"w",98};

printf("%d %c %d", s1.a, s1.arr[0], s1.d);
//变量名.结构体成员
//打印20 w 98

8.2 结构体指针访问

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{	
	struct stu  //结构体类型为struct stu
	{
		int a;
		char arr[20];
		int d;
	}s1 = { 20,{"w"},98 };
	struct stu* p1 = &s1; //将结构体变量s1的地址 存放进p1中

	printf("%d %d %c",p1->a,p1->d,p1->arr[0]);
    //结构体指针->结构体成员
	//打印20 98 w

9.结构体内存存储 

9.1 结构体内存存储4条规则

结构体内存储存方式有以下4条规则,也叫做结构体内存对齐原则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数    = 编译器默认的第一个对齐数与该成员变量大小的较小值
vs中默认的值为8
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍
4.如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍,结构体的整数大小就是所有最大对齐数(含嵌套结构体中成员的对齐数的整数倍)

多说无益,我们拿 代码例题+配图 来讲解

例题1&&讲解:

	//例题1:
	struct S1 
	{
	char c1;	
	int i;
	char c2;
	};
	printf("%zd ", sizeof(struct S1));
    //打印12

首先结构体的内存存储默认从0开始存储,我们以例题讲解

结构体内容的储存方式:

第一个结构体内容为char c1,结构体成员 c1 占用1个字节大小,结构体默认从0位置开始存储,那就从0开始存储1个字节大小的内存

第二个结构体内容为int i,结构体内容 i 占用4个字节,但因为结构体内存对齐原则,占用4个字节的int i,只能从 4 的整数倍数处(也叫结构体成员 i 的对齐数的整数倍)开始存储内存,所以我们直接跳到 4 的位置,从4的位置开始向下存储4个字节大小,4 5 6 7都是 i 的内存存储处

第三个结构体内容为char c2,结构体成员 c2 占用一个字节,因为结构体内存对齐原则,占用1个字节大小的char c2只能从1 的整数倍数处(对齐数的整数倍)开始存储内存,所以接着从 7 的位置向下存储1个字节大小的内存,即:将8的位置设为char c2 的内存存储处


结构体字节的大小:

在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为4,也就是int i 中的 i 的对齐数为4,

因为结构体内存存储占用了9个字节的大小,所以我们取4的整数倍数(最大对齐数)12,最终大小被填充到12个字节

7c148244ec734f7f8649a743576f0a03.jpeg

 例题2&&讲解:

	//例题2:
	struct S2 
	{
	char c1;	
	char c2;
	int i;
	};
	printf("%zd", sizeof(struct S2));
	//打印8

结构体内容的储存方式:

第一个结构体内容为char c1,结构体成员c1占用1个字节大小,结构体默认从0位置开始存储,那就从0开始存储1个字节大小的内存

第二个结构体的内容为char c2,结构体成员c2占用1个字节,但因为结构体内存对齐原则,占用1个字节的char c2,只能从 1 的整数倍数处(也叫c2 的对齐数的整数倍)开始存储内存,所以向下1个字节的位置为c2的内存存储地址

第三个结构体内容为int i,结构体成员 i 占用4个字节,因为结构体内存对齐原则,占用4个字节大小的int i 只能从4 的整数倍数处(对齐数的整数倍)开始存储内存,所以我们从 4 的整数倍处开始存储4个字节,4 5 6 7都为结构体变量内容 i 的存储地址

结构体字节的大小:

在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为4,也就是int i 中的 i 的对齐数为4,

因为结构体内存存储占用了8个字节的大小,所以我们取4的整数倍数(最大对齐数)8,最终大小被填充到8个字节

45adfa2174674b4da65667ef24817862.jpeg

例题3&&讲解:

	struct S3 
	{
		double d;
		char c;
		int i;
	};
	printf("%zd", sizeof(struct S3));
	//打印16

 结构体内容的储存方式:

第一个结构体内容为double d,结构体成员 d 占用8个字节大小,结构体默认从0位置开始存储,那就从0开始存储8个字节大小的内存

第二个结构体的内容为char c,结构体成员 c 占用1个字节,但因为结构体内存对齐原则,占用1个字节的char c,只能从 1 的整数倍数处(也叫c 的对齐数的整数倍)开始存储内存,所以向下1个字节的位置为 c 的内存存储地址

第三个结构体内容为int i,结构体成员 i 占用4个字节,因为结构体内存对齐原则,占用4个字节大小的int i 只能从4 的整数倍数处(对齐数的整数倍)开始存储内存,所以我们从 4 的整数倍处开始存储4个字节,因为8的位置已经被存储了,所以我们从下一个整数倍数--也就是12的位置开始存储,12 13 14 15都为结构体变量内容 i 的存储地址

结构体字节的大小:

在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为8,也就是double d 中的 d 的对齐数为4,

因为结构体内存存储占用了16个字节的大小,所以我们取最大对齐数 8 的整数倍数(最大对齐数)16,最终大小被填充到16个字节

58c6dc6b4839405a99b665ff8f6908cb.jpeg

例题4&&讲解(结构体嵌套问题 )

	struct S3 {
		double d;
		char c;
		int i;
	};
	//S3字节为16

	struct S4
	{
		char c1;
		struct S3 s3;
		double d;
	};
	printf("%zd ", sizeof(struct S4));
	//打印32

 结构体内容的储存方式: 

第一个结构体内容为char c1,结构体成员 c1 占用1个字节大小,结构体默认从0位置开始存储,那就从0开始存储1个字节大小的内存

第二个结构体的内容为struct S3 s3,结构体成员 s3 占用16个字节,但因为是结构体嵌套的内存存储,所以我们要从s3中成员最大对齐数的整数倍处开始存储,s3中最大对齐数的整数倍为8,所以我们从8的位置开始往下存储16个字节,直到23

第三个结构体内容为double d,结构体成员 d 占用8个字节,因为结构体内存对齐原则,占用8个字节大小的double d 只能从8 的整数倍数处(也叫对齐数)开始存储内存,所以我们从 8 的整数倍处开始存储8个字节,也就是从24的位置开始,向下存放8个字节大小的内存

结构体字节的大小:

在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为8,也就是double d 或者 s3中double d  的对齐数 8,

因为结构体内存存储占用了32个字节的大小,所以我们取最大对齐数 8 的整数倍数32,最终大小被填充到32个字节

e5f1b12faf7345308f55da5b363ded9c.jpeg


 10.为什么存在内存对齐?

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


 11.如何节省结构体内存大小

想要节省结构体内存的大小,我们需要同时满足对齐,又要节省空间

我们可以通过将占用空间小的结构体成员尽量集中在一起

例如:

struct S1 {
	char c1;
	int i;
	char c2;
};
//S1占用12个字节

struct S2 {
	char c1;
	char c2;
	int i;
};
//S2占用8个字节

S1和S2的结构体成员一模一样,但是S2占用的字节比S1小


 12.修改默认对齐数

在结构体中,我们可以使用一个预处理指令来修改结构体的默认对齐数

#pragma

#pragma这个预处理指令可以改变编译器的默认对齐数,这对于节省结构体内存非常有用

#pragma改变默认对齐数后,所有结构体内容的对齐数都会被#pragma修改

使用示例如下:

#pragma pack(1)//设置结构体默认对齐数为1
	struct S {
		char c1;
		int i;
		char c2;
	};

	struct S1 {
		double d;
		char c;
		int i;
	};
#pragma pack()//恢复结构体默认对齐数
	printf("%d\n", sizeof(struct S));
	//打印6
	printf("%d\n", sizeof(struct S1));
	//打印13

修改结构体默认对齐数后,所有结构体成员都会受影响,因此结构体成员的最大对齐数也会随之改变 

分析如图:

因为结构体成员的最大对齐数为1,所以结构体内存就以最大对齐数的整数倍(也就是 1 的整数倍)为结构体字节大小

027b6165cc294e1f84c61f8255f77f49.jpeg89d0313f7e134ed789740fc5df90ba1f.jpeg


 13.scand初始化结构体变量

使用scanf初始化结构体变量,就需要对变量内的内容直接访问,以初始化结构体变量内的内容

	struct stu {
		int a;
		char arr[20];
	}s1;
	scanf("%d%s", &s1.a, s1.arr);
	//s1.arr,代表的是s1中arr数组首元素的地址,所以不需要使用 & 操作符
	//输入20 abc
	printf("%d %s", s1.a, s1.arr);
	//打印20 abc

 错误写法如下079797e7364d4b95b6041ebdf45803b1.png

d3a13b0f06af41bf8e7b82802130e757.png

使用&s1是不可以完成赋值结构体变量的内容的,因为结构体变量内存的存储方式多种多样,结构体变量的地址与结构体变量第一个内容的地址相同,但之后的变量内容怎么办?无法通过scanf一次性输入

所以不能使用&s1来实现结构体的输入

14.结构体传参

结构体传参传的是什么?

结构体传参有两种传法,一种是将整个结构体内容全部传过去,另一种则是将结构体变量的地址传过去

示例如下:

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>

struct stu  //结构体类型为struct stu
{
	int a;
	char arr[20];
	int d;
}s1 = { 20,{"w"},98 };

void print1(struct stu s1) {
	printf("%d %c %d\n", s1.a, s1.arr[0], s1.d);
    //打印 20 w 98
}

void print2(struct stu* ps) {
	printf("%d %c %d\n", ps->a, ps->arr[0], ps->d);
   //打印 20 w 98
}
int main()
{
	
	print1(s1);
	print2(&s1);
}	

注意:结构体传参时,结构体必须为全局变量,如果为局部变量,结构体传参就会报错,如下图:

cdc71467c76748bda13e385d47bc2ee4.png


问:print1 与 print2 哪个更好些?

答案是首选print2函数,传递结构体指针的地址更好

原因:函数传参时,参数是需要压栈的,会有时间和空间上的开销

如果我们将一整个结构体传过去,结构体的内存越大,参数压栈的系统开销就越大,会导致性能的下降

结论:结构体传参时,首选传递结构体的地址


2a775d9141e049b28a499aa8c5166913.png

  • 24
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值