自定义类型——结构体(C语言进阶)

目录

一、结构的声明

1.1 一般声明

1.2 特殊声明

1.3 结构体自引用

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

 2.1 结构体变量的定义

2.2 结构体变量的初始化

三、结构体内存对齐

3.1 代码分析

3.2 结构体内存对齐的规则

 3.3 嵌套结构体的大小

3.4 存在结构体内存对齐的原因

3.5修改默认对齐数

四、结构体传参

        结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。这里小编将带着大家一起来了解学习结构体,分别从结构体的声明、自引用,结构体的定义和初始化,以及结构体内存对齐和结构体传参这些方面来学习结构体。

一、结构的声明

1.1 一般声明

struct tag
{
    member-list;
}variable-list;

        结构体的语法形式如上代码,struct是关键字,tag是个标签名,这个是可以自定义的,根据自己实际情况来进行定义的,紧跟着是大括号,里面是成员列表,最后大括号外面是变量列表。举个例子吧,用结构体来描述学生的相关信息。

struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1,s2,s3; //分号不能丢

        这里的name、age、sex和id都是成员列表。s1、s2、s3是结构体变量,当然也可以在下面重新命名,例如:

struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢

int main()
{
    struct Stu s1,s2,s3;
    return 0;
}

1.2 特殊声明

        在声明结构体的时候,可以不完全声明。

例如:

struct
{
	int a;
	char b;
	float c;
}x;
int main()
{
	 
	return 0;
}

        如上代码,struct后面没有标签名,只是在整个结构体后面直接命名了一个x作为结构体的变量名,这种就属于匿名结构体类型,其实这种匿名结构体类型一般情况只能用一次,在后面代码中不能再次创建结构体了。

        在此基础上,我们在创建一个成员相同结构体,以结构体指针的形式命名,如下代码,

struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}* p;
int main()
{
	p = &x;//错误写法
	return 0;
}

此时我们会认为,这两个结构体成员一模一样,然后我们用p来接收x的地址,其实这是错误的。

注意:这样编译器会发出警告,警告说p的类型和&x的类型不兼容,因为我们这两个结构体连标签都没有,编译器会认为这是两种不同结构体。这种匿名结构体类型绝大部分情况下我们不会去使用。

1.3 结构体自引用

自定义类型——结构体(C语言进阶)_第1张图片

         结构体的自引用就类似数据结构中的链表,为了找到下个节点,在自己的成员列表中记录自己同类型的结构体的指针作为节点。

注意:必须是下个结构体的指针(地址),不能存放下个结构体的变量名

struct Node
{
	int data;//数据域
	struct Node* Next;//指针域
};

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

 2.1 结构体变量的定义

struct Point
{
    int x;
    int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2

定义结构体变量有两种方法:

①声明类型的同时定义变量

②重新定义结构体变量

2.2 结构体变量的初始化

struct Stu
{
	char ch;
	int a;
}s1={.a=100,.ch='x'},s2={.a=500,.ch='f'};
//s1={'w',100},s2={'s',200};

如上这种声明类型和定义结构体变量的同时初始哈结构体成员变量,我们可以使用' . '操作符来进行赋值,这样可以不按照顺序来赋值,也可以直接进行赋值初始化,但需要按照顺序。

当然结构体里面也可以包含其他的结构,初始化也是同理。代码如下:

struct Stu
{
	char ch;
	int a;
}s1, s2;

struct School
{
	float d;
	struct Stu s1;
	int x;
	int arr[3];
};
int main()
{
	struct School Sn = { 3.14,{'m',666},999,{4,5,6} };
	return 0;
}

三、结构体内存对齐

3.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;
}

当我们看到上面的题目时候,感觉这两个结构体的是一样的,就只是成员变量顺序换了一下罢了,我们会以为这两个结构体的大小是一样大的,char是一个字节,int是四个字节,所以会以为这两个结构体的大小都是6字节,当我们运行代码之后,我们发现事实却不是这样的。

自定义类型——结构体(C语言进阶)_第2张图片

第一个结构体的大小是12字节,第二个结构体的大小是8字节,为什么是这样的呢,这里就涉及到结构体内存对齐的问题。 

这里先介绍一个宏,叫做 offsetof ,这个宏可以计算结构体某个成员相较于结构体起始位置的偏移量。(如下图)

两个参数,第一个参数是结构体类型,第二个参数结构体成员,头文件

自定义类型——结构体(C语言进阶)_第3张图片

 代码如下:

#include
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;
}

 我们发现,c1距离结构体起始位置的偏移量是0,i距离结构体起始位置的偏移量是4,c2距离结构体起始位置的偏移量是8,如下图所示

自定义类型——结构体(C语言进阶)_第4张图片

但是这样算下来,应该是9个字节呀,为什么显示的是12个字节呢?其实本质上,在最后又浪费了3个字节,使得最后是12个字节。接下来,我们来研究一下为什么是这样呢?

3.2 结构体内存对齐的规则

① 第一个成员在与结构体变量偏移量为0的地址处。
② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
        VS中默认的值为8
        Linux中没有默认对齐数,对齐数就是成员自身的大小
③ 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

利用这些规则,来解读一下上面的疑惑,第一个成员c1放在了结构体的第一个空间的位置,也就是与结构体偏移量为0的地址,然后从第二个成员开始,往后每一个成员都要对齐到某个对齐数的整数倍处。这里小编使用的是VS,默认对齐数是8,针对我们这个代码来说,第二个成员变量i的自身大小为4,编译器的默认对齐数是8,所以i的对齐数为4,所以所放的位置应该对齐到4的倍数处,此时会发现前面有3个字节的空间浪费掉了,再看第三个成员变量,通过计算c2的对齐数1,这时直接向后放就可以了,那么这里已经使用9个字节了,怎么算的12呢,这时就要利用第三个规则了,第一个变量对齐数是1,第二个变量的对齐数是4,第三个变量的对齐数1,所以最大对齐数是4,结构体总大小为最大对齐数的整数倍,由此可以得出,结构体的总大小为12。如下图所示,

自定义类型——结构体(C语言进阶)_第5张图片自定义类型——结构体(C语言进阶)_第6张图片

 3.3 嵌套结构体的大小

        在上面,我们把结构体在内存对齐的规则讲完后,就把最开始的几个代码和疑惑全部解释清楚了,但是规则4我们还没有利用到,这里小编再聚一个例子来解读一下规则4.

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double e;
};

看到上面代码,通过规则我们可以轻易的算出s3结构体的大小为16字节。那么结构体s4的大小为多少呢,首先c1占用第一个字节,然后成员s3,s3的最大对齐数(s3自己所有成员的对齐数的最大值)为8,自身大小为16,所以对齐数为8,所以s3要放在8的整数倍处,然后在放e,计算完发现此时使用了32个字节,最后结构体的总大小为最大对齐数,也就是8的倍数,发现32刚好是32的倍数,所以最后结构体s4的大小为32。

        好了,讲到这里,结构体内存对齐的规则已经讲清楚了,但是,为什么会存在结构体对齐这个东西呢?

3.4 存在结构体内存对齐的原因

大部分资料是这样说的。

①平台原因(移植原因):
        不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
        数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
        结构体的内存对齐是拿空间来换取时间的做法。

3.5修改默认对齐数

        其实默认对齐数是可以修改的。这里给大家介绍一个预处理指令。

#pragma pack(4) //修改默认对齐数4

注意:①一般情况下,我们修改的默认对齐数都是2的次方数,很少出现3,5这样的默认对齐数

        ②当我们修改默认对齐数并使用完之后,一定要取消设置默认的对齐数。

举个例子来为大家说明吧。

#pragma pack(1)//讲默认对齐数改为1
struct S5
{
	char ch1;
	int x;
	char ch2;
};
#pragma pack()  //取消设置默认对齐数
int main()
{
	printf("%d\n", sizeof(struct S5));
	return 0;
}

 如上,当我们修改完默认对齐数之后,再利用规则就可以算出,结构体S5的大小为6。

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

四、结构体传参

        最后,我们来学习一下结构体传参。有时候我们创建一个结构体变量之后,不会直接使用它,而是作为参数,来进行传参。

①直接以结构体变量名作为参数

②以结构体的地址作为参数

如下代码:

struct S
{
	int data[1000];
	int num;
};
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);//点操作符解引用
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);//指针通过->解引用
}
int main()
{
	struct S s = { {1,2,3,4}, 1000 };

	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

如上代码,我们通过两种方式传参都可以实现我们的效果。那么到底哪一种更合适呢,哪一种更好呢?其实是穿结构体指针更好一些,为什么呢?

        当我们以结构体变量作为实参来传参的时候,实参传给形参的时候,我们知道,形参是实参的一块临时拷贝,它也需要准备一个很大的空间来存放拷贝过来的数据。函数传参的时候,系统会进行压栈的,这些系统消耗会比较大,浪费空间。

        当我们以指针的形式传参的时候,系统只会开辟4/8个字节,消耗会比较小,省空间。

结论:

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

也就是我们要传结构体指针。

        好了,到这里,今天要讲的结构体相关的知识就讲完了,希望这篇文章对你起到一定的帮助,如果觉得小编写的还可以的,可以一键三连(点赞,关注,收藏)哦,你们的支持是对小编极大的鼓励,谢谢!!!

自定义类型——结构体(C语言进阶)_第7张图片

 

 

你可能感兴趣的:(c语言,开发语言)