C语言之走进结构体

目录

1.结构体的声明

1.1结构体的基本基础知识

1.2结构的声明

1.3特殊的声明

1.4结构体的自引用

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

2.结构体的内存对齐

2.1结构体内存对齐规则

2.2验证结构体在内存中的存储方式

2.3为什么存在内存对齐

2.4修改默认对齐数

3.结构体传参


1.结构体的声明

1.1结构体的基本基础知识

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

我们可能会发觉这与数组有点类似,但数组是相同类型的值的集合

1.2结构的声明

若结构体变量定义在主函数外面,它就是一个全局变量,若定义在主函数里面,就是局部变量。

struct tag
{
  member_list;//成员列表
}variable_list;//变量列表(全局变量)
int main()
{
  struct stu s3;//局部变量
}

1.3特殊的声明

匿名结构体,这是一种特殊的结构体,必须在创建好之后立马创建一个全局变量,否则这个结构体无法使用。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

//匿名结构体
struct
{
	char name[20];
	int age;
}s1;
int main()
{
	return 0;
}

我们再来看下面这俩个结构体,除了结构体变量名称不同外其余都相同,但当我们想把s2的地址赋给指针p时,我们会发现这个操作是不被允许的,因为这是不合法的,编译器会把上面两个声明当成完全不同的两个类型。

struct
{
	char name[20];
	int a;
	double c;
}s2;
struct
{
	char name[20];
	int a;
	double c;
}*p;
p = &s2;

1.4结构体的自引用

//第一种自引用

struct Node
{
	int data;
	struct Node* next;//可行,一个指针指向下一个指针,节省空间
	//struct Node next 这种方式不推荐,因为不知道这个结构体多大,占用空间无限大
};

//第二种名称重定义自引用

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

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

struct Point
{
	int x;
	int y;
}p1={10,20};//结构体全局变量初始化

struct S
{
	int num;
	char ch;
	struct Point p;
	float d;
};
int main()
{
	struct Point p2 = { 0,0 };//结构体局部变量初始化
	struct S s = { 100,'w',{2,5},3.14f };
	printf("%d %c %d %d %f", s.num, s.ch, s.p.x, s.p.y, s.d);
	return 0;
}

2.结构体的内存对齐

知道了结构体的基本知识,我们就可以学习结构体在内存中是如何存储的了,下面让我们以一段代码例子来引出结构体内存对齐规则。

#define _CRT_SECURE_NO_WARNINGS 1
#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;
}

我们发现S1和S2中成员变量类型相同,只是运行顺序不一样,但是当我们运行程序时,发现两个结构体大小是不同的:

此时,就引出了我们的结构体内存对齐规则。

2.1结构体内存对齐规则

结构体内存对齐规则:

1.结构体的第一个成员,对齐到结构体在内存中存放位置的0偏移量处;

2.从第二个成员开始,每个成员都要对齐到(一个对齐数)的整数倍处;

  对齐数:结构体成员自身大小和默认对齐数的较小值。

   VS中,默认对齐数是8;Linux gcc中没有对齐数,对齐数默认是结构体成员自身大小。

3.如果结构体中嵌套了结构体成员,要将嵌套的结构体成员对齐到成员自己的成员中最大对齐数的整数倍处;

4.结构体的总大小,必须是所以成员的对齐数中最大对齐数的整数倍;

这里的最大对齐数是:包含嵌套结构体成员中的对齐数的所有对齐数中的最大值

此时,我们根据这些规则分析一下S1和S2

先看S1,

1.根据第一条规则,我们把c1放在内存0偏移量处;

2.然后根据第二条规则,我们知道对齐数是4,因为我是在VS环境下编译的,默认对齐数是8,然后整型大小是4,而对齐数是结构体成员自身大小和默认对齐数中的较小值,所以就是4,这就说明,我们要把i这个成员存放在4偏移量处,浪费掉了与c1之间的3个空间;

3.然后我们再放c2,也是根据第二条规则,我们知道它的对齐数是1,所以,c2放到了8偏移量处;

4.因为我们这个结构体没有嵌套结构体使用,所以我们直接看最后一条,结构体的总大小,必须是所以成员的对齐数中最大对齐数的整数倍,我们现在是占了9个字节,所有对齐数最大的成员是int型,也就是4,现在已经是占了9个字节了,所以只能是4的3倍也就是12个字节才能存储下这个结构体,然后剩余没有存放数据的空间是被浪费掉的。

S2也是同样的道理,大家可以自己试着分析一下:

分析了结构体中没有嵌套结构体成员之后,我们再来分析一个嵌套结构体成员的例子:

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%d\n", sizeof(struct S4));
	return 0;
}

我们求S4在内存中的大小,运行后发现是32:

前面的存储方式和S1是一样的,这里只需要在注意规则的第三条,如果结构体中嵌套了结构体成员,要将嵌套的结构体成员对齐到成员自己的成员中最大对齐数的整数倍处;相信大家看了我们分析图也可以看懂,(如果不清楚的话,欢迎大家私信)。

2.2验证结构体在内存中的存储方式

我们有些小伙伴可能还是对这个存储方式表示怀疑,结构体在内存中真的是这样存储的吗?其实是有方法可以验证的:

这里我们介绍一个函数,offsetof,用于计算一个结构体成员相较于起始位置的偏移量,大家可以去我之前推荐的网站上详细了解

根据这个函数,我们就可以求偏移量了,这里我们以我们的struct S1为例,我们已经知道结构体成员的偏移量分别为0,4,8,接下来我们来验证一下:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stddef.h>

struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));
	return 0;
}

我们发现,与内存对齐偏移量相符合,证明了结构体在内存中确实是这样存储的。

2.3为什么存在内存对齐

1.平台原因

不是所以的硬件平台都能访问任意地址上的任意数据:某些硬件平台智能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因

数据结构(尤其是栈)应该尽可能的在自然边界上对齐,原因在于为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问。这样说可能有点抽象,我们以一个例子来理解:

struct S
{
	char c;
	int i;
};

这时,我们要读取i

在未对齐方式下:

在对齐方式下:

总而言之,结构体的内存对齐是拿换取时间的做法。

同时,在设计结构体的时候,我们既要买满足对齐,又要节省空间,就应该让占用空间小的成员尽量集中在一起,就比如我们的第一个例子,struct S1(占用12个字节)和struct S2(占用8个字节),它们的成员类型一样,但是S1和S2所占空间的大小却有了一些区别。

2.4修改默认对齐数

默认对齐数是可以被修改的,结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。这里需要用到一个预处理指令:#pragma pack(),它可以修改默认对齐数,括号里面的有效值为1、2、4、8、16:

例如我们要把默认对齐数修改为1,这里以S1,S2为例:

#pragma pack(1)
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;
}
#pragma pack()//取消默认对齐数,还原为默认

我们运行发现,结构体的大小确实发生了改变:

3.结构体传参

struct S
{
	int data[100];
	int num;
}s={{1,2,3,4},100};//结构体初始化
//结构体传参
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;
}

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


这就是目前结构体的所有内容了,接下来会用结构体实现一个通讯录,大家有兴趣可以期待一下!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月亮夹馍干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值