自定义类型【1】(结构体详解,结构体内存对齐有详细图示哦)

本文详细介绍了C语言中的结构体,包括结构体的声明(普通和匿名),自引用结构体的实现,结构体变量的定义与初始化,以及结构体在函数参数传递中的使用。此外,重点讲解了结构体内存对齐的规则和原因,以及如何通过预处理指令修改默认对齐数。最后,文章强调了内存对齐对性能的影响和节省空间的策略。
摘要由CSDN通过智能技术生成

引言

到现在,我们已经能够熟练地运用数组。数组的每个元素是相同类型的数据。而结构体的成员可以是不同类型的数据。

在初识C语言部分,我们已经对结构体有了一个初步的认识。初识C语言
在本篇文章中就详细介绍一下结构体:

结构体的声明

结构体的声明

如果我们要描述一个学生,我们就可以创建一个结构体变量。这个结构体变量中存放学生的姓名、性别、年龄、学号等信息。对于姓名、性别、学号我们可以用字符数组类型;对于年龄可以用整型。于是我们可以声明一个这样的结构体变量。

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
};

在这段代码中,我们创建了一个结构体类型,类型名为struct stu。但是仅仅是创建了一个类型,还没有定义这个类型的结构体变量。也就是说此时还没有为这个结构体类型开辟内存空间。

我们也可以在声明这个结构体的时候就创建结构体变量:

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
}zhangsan;

在这段代码中,我们在创建结构体类型的同时创建了zhangsan这个结构体变量。并且此时已经zhangsan这个变量开辟了一块空间,类型是struct stu。这个类型在书写的时候有一些麻烦,我们也可以用typedef来对这个结构体类型重命名:

typedef struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
}stu;

在";"前是重命名后的类型名。但是如果要在结构体声明的时候对其重命名的话,就不能在声明时创建结构体变量了。

特殊的结构体声明

在声明结构体时,可以不完全声明。即匿名结构体类型:

struct 
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
};

在这段代码中,声明了一个结构体类型。这个结构体类型的类型名是不完整的,只有结构体关键字没有结构体标签,所以只能在创建类型的时侯就创建该类型的结构体变量。

就算两个匿名结构体的成员类型一样,编译器也会将这两个变量当成不同的类型:

struct 
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
}s;
struct
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
}*ps;
int main()
{
	ps = &s;
	return 0;
}

如果你写出了这样的代码,那么编译器就会警告类型不兼容。

并且这个结构体类型只能使用一次,下一次就不能再使用了。

结构体的自引用

结构体的自引用就是结构体自己引用自己,类似于链表的结构。在链表的每一个节点都包含这个节点的数据与找到下一个节点的指向。
我们就可以让这个结构体作为它本身的一个成员:

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

但是,这样的结构体声明是存在很大问题的。当结构体作为它本身的成员时,要给这个结构体开辟的内存空间里面要包括一个整型变量和一个和它本身一样的结构体变量,这个结构体里也包含一个整型变量和一个结构体变量…这个结构体的大小就无限大了。

我们知道,指针变量的大小都是4或8个字节。我们只要将结构体本身的指针作为结构体成员即可。我们就可以这样实现:

struct Node
{
	int data;
	struct Node* pnode;
};

在这个struct Node结构体中,既包括了一个数据,也包括了下一个节点的指向。

需要注意的是:当我们对这个自引用的结构体用typedef重命名的时候不能将重命名后的结构体指针名直接作为结构体成员。在typedef时,被重命名的类型必须是完整的,否则编译器会不认识:

//错误
//typedef struct Node
//{
//	int data;
//	Node* pnode;
//}Node;

//正确
typedef struct Node
{
	int data;
	struct Node* pnode;
}Node;

结构体变量的定义与初始化

结构体变量的定义

在定义结构体变量时,可以在创建结构体类型的时候定义结构体变量(如刚才提到的);也可以在后面使用这个创建的结构体类型来定义结构体变量:

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
}zhangsan;//创建结构体类型时定义结构体变量

int main()
{
	struct stu lisi;//之后定义结构体变量
	return 0;
}

结构体变量的初始化

但是这俩个结构体变量都没有被初始化。在全局定义的结构体变量zhangsan被初始化为0,而局部定义的lisi则是任意值。

我们可以在定义这个变量的时候就给它赋值。结构体的赋值与数组类似,需要用{ }括起来,每个成员之间用","隔开:

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
}zhangsan = { "zhangsan", "nan", 18, "0543122" };

int main()
{
	struct stu lisi = { "lisi", "nv", 19, "0543123" };
	return 0;
}

当然,用"."访问结构体成员再赋值也是ok的。

需要注意的是:当对一个嵌套定义的结构体类型赋值时,作为成员的结构体需要单独用{ }初始化,并用","与其他成员隔开:

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
};
struct Class
{
	struct stu zhangsan;
	struct stu lisi;
};

int main()
{
	struct  Class class1 = { { "zhangsan", "nan", 18, "0543122" }, { "lisi", "nv", 19, "0543123" } };
	return 0;
}

结构体传参

当函数需要使用结构体作为参数时。我们可以选择直接传递结构体变量(传值);也可以选择传递结构体变量的指针(传址):

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
};
struct Class
{
	struct stu zhangsan;
	struct stu lisi;
};

void test1(struct Class class1)//以结构体作为参数(传值)
{}
void test2(struct Class* pclass1)//以结构体指针作为参数(传址)
{}
int main()
{
	struct  Class class1 = { { "zhangsan", "nan", 18, "0543122" }, { "lisi", "nv", 19, "0543123" } };
	test1(class1);
	test2(&class1);
	return 0;
}

我们来分析这两段代码:

在前面的初识C语言部分,我们已经对函数的传参有了一定的了解:传址调用时,形参是实参的一份临时拷贝,不能通过形参改变实参的数据;传址调用时传递变量的地址,可以在函数内部改变数据。

但是我们仔细思考就会发现:传址调用时,形参也是实参的一份零时拷贝,只不过这个拷贝的内容是数据的地址。我们可以在函数内部通过这个零时拷贝来的指针改变其指向的数据,如果想要改变这个指针变量的内容,当然也是不行的。

对于结构体传参:传递结构体本身是零时拷贝一份与结构体相同大小的空间(可能会很大。就例子而言至少也需要76字节的空间);而传递结构体指针则是零时拷贝一份4或8字节的空间。考虑到内存的使用效率,在结构体传参时,建议传递结构体指针,也就是传值调用。

结构体内存对齐

上面提到struct Class结构体类型的大小至少为76字节,是因为在这个结构体中包括两个结构体变量,每一个结构体变量中都含有3个字符数组与一个整型。那么,这个struct Class结构体类型的大小到底是多少呢?

我们可以用关键字sizeof来计算:

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
};
struct Class
{
	struct stu zhangsan;
	struct stu lisi;
};

int main()
{
	printf("%d\n", sizeof(struct Class));
	return 0;
}

在这里插入图片描述
结果显然与我们的猜想不符,但也确实大于76字节。

这是因为结构体类型在存储时需要遵循结构体内存对齐:

结构体内存对齐

规则

首先来介绍一下结构体内存对齐的规则:

1、第一个成员在此结构体偏移量为0的位置处开始开辟空间;
2、其他成员变量要对齐到对齐数的整数倍的地址处开始开辟内存空间(对齐数是编译器的默认对齐数与成员变量大小的较小值。vs的默认对齐数为8);
3、结构体的总大小是结构体成员变量对齐数最大值的整数倍;
4、如果该结构体嵌套了结构体,此时该成员结构体的对齐数是成员结构体中的结构体成员的对齐数的最大值与默认对齐数的较小值;
5、如果结构体中有成员是数组,此时该数组的对齐数是数组元素的大小与默认对齐数的较小值。

示例

用规则来计算一下上例中的结构体struct Class的大小:

struct stu
{
	char name[10];
	char sex[4];
	int age;
	char id[20];
};
struct Class
{
	struct stu zhangsan;
	struct stu lisi;
};

对于结构体struct Class。首先第一个成员是一个结构体,类型为struct stu,这个成员从偏移量为0的位置开始开辟空间。

接着,我们需要计算结构体struct stu的大小:
struct stu的第一个成员是char类型的数组,从struct stu中偏移量为0的位置开始开辟空间大小为1*10=10字节。所以这个成员的内存开辟在偏移量0到9的空间;
第二个成员也是字符数组,对齐数为char型的大小1与8的较小值,即1(我用的是vs环境)。开始开辟空间的偏移量要是对齐数1的倍数,大小为1*4=4字节。所以这个成员的内存开辟在偏移量10到13的空间;
第三个成员的类型是int,对齐数为4与8的较小值4。开始开辟空间的偏移量要是对齐数4的倍数,大小为4字节。所以这个成员的内存开辟在偏移量16到19的空间(注意,这时由于对齐,偏移量为14与15的空间浪费了);
第四个成员也是字符数组,对齐数为char型的大小1与8的较小值,即1。开始开辟空间的偏移量要是对齐数1的倍数,大小为1*20=20字节。所以这个成员的内存开辟在偏移量20到39的空间;
计算到这里,struct stu结构体的大小为40字节。结构体的总大小需要是成员对齐数的最大值的整数倍,即1、1、4、1中最大值4的整数倍。40显然符合,所以struct stu结构体的大小就为40字节。
在这里插入图片描述

所以,struct Class结构体的第一个成员所开辟的空间就是偏移量为0到39的40个空间;

struct Class的第二个成员也是struct stu型的。它的对齐数是成员结构体中的成员的对齐数的最大值与默认对齐数的较小值。也就是1、1、4、1中最大值4与8的较小值,也就是4。开始开辟空间的偏移量要是对齐数4的倍数,大小为40字节。所以这第二个结构体成员的内存就在偏移量40到79的空间开辟。

到现在,结构体struct Class的大小是80个字节。结构体的总大小需要为成员对齐数的最大值的整数倍:即4、4最大值4的整数倍。80显然符合,所以,结构体struct Class的大小就是80个字节。

在这里插入图片描述

原因

很明显,结构体内存对齐会导致有一部分内存浪费了。其实这个设计有两个可能的原因:

1、平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。

2、性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问。而对齐的内存访问仅需要一次访问。

总的来说就是内存对齐可以空间换时间,使效率更高。

为了既节省空间,又提高效率。我们可以在创建结构体类型的时候将相同类型的成员尽量放在一起来尽量减少内存的浪费。
例如:

struct test1
{
	int a;
	char b;
	char c;
	char d;
};
struct test2
{
	char b;
	int a;
	char c;
	char d;
};

int main()
{
	printf("%d\n", sizeof(struct test1));
	printf("%d\n", sizeof(struct test2));
	return 0;
}

在这里插入图片描述
图示如下:
在这里插入图片描述

修改默认对齐数

编译器的默认对齐数是可以通过预处理来修改的:

//将默认对齐数修改为2
#pragma pack(2)
//恢复默认对齐数的初值
#pragma pack()

我们可以在修改为2的环境下再次运行上述例子:

//将默认对齐数修改为2
#pragma pack(2)

struct test1
{
	int a;
	char b;
	char c;
	char d;
};
struct test2
{
	char b;
	int a;
	char c;
	char d;
};

int main()
{
	printf("%d\n", sizeof(struct test1));
	printf("%d\n", sizeof(struct test2));
	return 0;
}

在这里插入图片描述
图示:
在这里插入图片描述
当然,如果将默认对齐数改为1时,就失去了内存对齐的意义。

总结

在这篇文章中,我们了解了关于结构体的声明、定义、传参以及计算大小的一些内容。在下一篇文章中将会详细介绍关于自定义类型的其他知识,欢迎持续关注哦

写这样的一篇博客基础知识的博客是很难有亮点的。我所能做的就是尽量让知识更加有逻辑性;将其解释的更加的便于理解。

最后,如果对本文有任何问题,欢迎在评论区进行讨论哦

希望与大家共同进步哦

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿qiu不熬夜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值