自定义类型:结构体(内存对齐)

本文详细介绍了C语言中结构体的声明方式、结构体变量的创建和初始化,以及内存对齐规则及其原因,包括对齐数的影响、性能优化策略和如何修改默认对齐数。
摘要由CSDN通过智能技术生成

目录

1.结构体类型的声明

1.1结构体

 1.2结构的声明

1.2.1一般声明

1.2.2 结构的特殊声明

2.结构体变量的创建和初始化

3.结构体内存对齐

3.1对齐规则

3.2为什么存在内存对齐?

3.3修改默认对齐数


1.结构体类型的声明

1.1结构体

C语言已经提供了内置类型,如:char, short, long, int, float, double等,但是只有这些内置类型还是不够的,假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造合适的类型。

结构是一些值的集合,这些值成为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至其他结构体。

 1.2结构的声明

1.2.1一般声明

例如描述一个学生:

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

1.2.2 结构的特殊声明

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

//匿名结构体类型

struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], * p;

上面两个结构体在声明的时候省略掉了结构体的标签(tag)

那么如果我们将x的地址传入p指针,即

p = &x;

在上面代码的基础上,这句代码合法吗?

警告: 

  • 编译器会把上面的两个声明当成完全不同类型的两个,所以是非法的。
  • 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

匿名结构体可以使用typedef创建名字。

例如:

#include<stdio.h>
typedef struct
{
	char c;
	int i;
	double d;
}S;
int main()
{
	S s;
	return 0;
}

不过,既然我们想用匿名结构体,我们现在又给它创建了名字,这样意义就不大了,匿名结构体在使用时基本上就是只使用一次。 

2.结构体变量的创建和初始化

#include<stdio.h>
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	//按照结构体成员的顺序初始化
	struct Stu s = { "张三",20,"男","20231221130" };
	printf("name:%s\n", s.name);
	printf("age :%d\n", s.age);
	printf("sex :%s\n", s.sex);
	printf("id  :%s\n", s.id);

	//按照指定的顺序初始化
	struct Stu s2 = { .age = 18,.name = "小赵",.id = "2023122265",.sex = "女" };
	printf("name:%s\n", s2.name);
	printf("age :%d\n", s2.age);
	printf("sex :%s\n", s2.sex);
	printf("id  :%s\n", s2.id);
	return 0;
}

输出的结果是:

3.结构体内存对齐

3.1对齐规则

首先得掌握结构体的对齐规则:

  • 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值

(注:VS中默认对齐数是8;

Linux中gcc没有默认对齐数,对齐数就是成员自身的大小)

  • 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。 

那我们来做几道练习来试试看吧!

#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

 在我们不了解对齐数的规则的时候,可能得出来的答案是6。

因为我们知道char类型占1个字节,int类型占4个字节,得出整体是6个字节,不过这样计算真的对吗?我们运行起来看一下。

那为什么是12呢?

  • 我们第一个成员char类型应该对齐到和结构体起始位置偏移量为0的地方,我们存下char类型的一个字节。
  • 接着我们要存int类型时,发现规则中说变量要对齐到对齐数的整数倍地址处,int类型本身的大小是4,而VS中默认对齐数是8,显然4是较小值,所以int类型成员的对齐数是4,我们应该找4的倍数,所以会浪费掉3个字节的空间(标红的地方),在偏移量为4的地方开始存int类型成员。
  • 当我们存完4个字节的int类型成员时,接着要存char类型,char类型的大小是1,默认对齐数是8,所以char类型成员的对齐数是1,我们在偏移量为8处存下char类型成员。
  • 所有成员变量都存完后,是9个字节大小,但是最终的结果不是9而是12,这就用到了规则中所说“结构体的总大小是最大对齐数的整数倍”,我们发现结构体成员的最大对齐数是4,最终结构体总大小应该是4的倍数,所以是12。

那么此时如果我调换一下成员的顺序,对输出结果的大小又有什么影响呢?

#include<stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

程序运行起来我们发现结果是8,不是12,这是为什么呢?

  • 先存储两个char类型的成员,由于对齐数都是1,所以没有空间的浪费。
  • 当我们在存int类型成员的时候,我们知道int类型成员对齐数是4,不可以接着从偏移量为2的地方开始存,我们需要找到4的倍数,应该是从偏移量为4处开始存放int类型的成员变量。
  • 存完int类型成员,整个结构体总大小刚好为8个字节,是最大对齐数4的整数倍,所以结果输出为8。

那么当我们遇到嵌套结构体的问题又该如何解决呢?

#include<stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));
	return 0;
}

先来看看运行的结果:

首先我们先计算一下结构体S3的大小:

数一下得出的结果是16个字节(double类型占8个字节)具体过程这里就不详细说明了,可以看看规则或者再看看上面两道题,应该这个就问题不大了。

重点我们看嵌套一个结构体的S4它的大小为什么是32个字节呢?

  • 我们先在偏移量为0的地址处存下char类型的成员。
  • 接着重点来了,我们该如何存储S3结构体呢?由上面的计算可得S3的大小是16个字节,而默认对齐数是8,较小值为8,所以S3的对齐数是8,接着我们找到偏移量为8的倍数的地方(也就是8),将S3存在偏移量为8的地方,往下存16个字节。
  • 存完S3结构体后,接着要存double类型的成员,而接下来的偏移量正好是8的倍数24,最后的结果是32,是最大对齐数8的倍数,所以输出32。

3.2为什么存在内存对齐?

大部分参考资料都是这样的:

1.平台原因(移植原因)

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

2.性能原因

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

总体来说: 结构体的内存对齐是拿空间来换取时间的做法。

 那在设计结构体的时候,我们既要满足对齐,又要节省空间,我们可以采取:

让占用空间小的成员尽量集中在一起。

//例如:
struct S1
{
	char c1;
	int i;
	char c2;
};

struct S2
{
	char c1;
	char c2;
	int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

S1占了12个字节,S2占了8个字节,明显S2更节省空间

3.3修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

#include<stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认

int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S));
	return 0;
}

默认对齐数改为1后,程序运行的结果就变成了:

差不多就讲到这里啦。

  • 24
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值