Bootstrap

C语言结构体内存对齐

结构体或许小伙伴们都知道,或许也能够做到熟悉的去运用结构体,但你们有没有想过:整型数组存放的数据都是整型,字符数组存放的数据都是字符,它们类型相同,所以也都能够做到在内存中紧密的存储而结构体中存放的数据各种各样,它们的存储是否能做到在内存中紧密排列呢?又或者说,结构体的内存应该怎样去计算呢?今天让我们来一起探讨一下这个问题~

首先,在了解结构体内存如何计算之前,我们先来了解一下存址的相关知识,高地址和低地址以及数据的高字节和低字节。

一、高地址与低地址

当我们把数据对应的指针存储在计算机给定空间时,为了方便查找会给地址进行编号,我们可以把它们看作一本书:第一页是0x00000001,第二页是0x00000002,第三页是0x00000003......一直到最后一页0x00000100.(这里是随便给的数,方便理解,并没有实际意义)。我们看书肯定是从第一页开始看,那么0x00000001就是低地址,相对的最后一页0x00000100就是高地址。

二、高字节与低字节

当我们小学学数学的时候,我们知道数字有个位,十位,百位。它们对应的值都成比例的越来越大,因此它们也分为高位和低位:一个三位数里百位就是高位,个位就是低位,这是十进制中的算法。当然相对的,二进制,十六进制也都分高位和低位:

而对应的这个二进制数中高位的0001就是高字节,1111就是低字节。也就是说高位就对应着高字节,低位对应着低字节

三、大端与小端存储模式

上面我们已经了解了高地址低地址,高字节低字节,那么字节存放在地址中应该以何种顺序存放呢?这就引出了我们的大端与小端存储模式。

大端(存储)模式: 是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。

小端(存储)模式: 是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。

(计算机读址的顺序是从低地址开始读向高地址)

我们假设一个16进制数字num为0x01 02 03 04,那么大端模式下将它存储,存放顺序就应该为:

而在小端模式下将它存储,存放顺序应该为:

大端存储和小端存储各有优缺点

大端存储的优缺点:大端存储在直观性和顺序性方面具有优势,但在传输效率和空间利用率方面略逊于小端存储。

小端存储的优缺点:小端存储虽然在节约空间和提高读写速度方面有优势,但违背了人类的直观认识,并可能引发平台依赖性问题。

四、结构体内存对齐

① 结构体内存对齐的规则

前面花费一些功夫把高低地址,高低字节以及大小端存储问题给大家讲解清楚了,终于可以引出我们这篇文章的主要内容:结构体内存对齐了。回想一下我们最开始提出的问题:结构体中存放的数据各种各样,它们的存储是否能做到在内存中紧密排列呢?又或者说,结构体的内存应该怎样去计算呢?让我们来举个例子:

struct Stu1 {
	int age;
	char name;
	int id;
	char sex;
};
struct Stu2 {
	char sex;
	char name;
	int age;
	int id;
};
int main()
{
	int num1 = sizeof(struct Stu1);
	int num2 = sizeof(struct Stu2);
	printf("num1长度为:%d\nnum2长度为:%d\n", num1, num2);
	return 0;
}

如果按照之前计算整型数组和字符数组大小的常规思路来判断,这两个结构体的大小应该是相等的,一个int型变量占4个字节,一个char型变量占1个字节,那么Stu1有两个int型变量和两个char型变量,大小理应为4+4+1+1=10,同样的Stu2也应为10。那让我们将代码运行一下看看是不是这样的:欸?奇怪了,不仅两个结构体的大小不为10,甚至两个结构体的大小都不相等,这是怎么一回事呢?其实结构体Stu1在内存中的存址形式是这样的:为什么会是这样的存储形式呢?这就是结构体内存对齐所造成的

结构体内存对齐的规则

结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

其他成员变量要对齐一个叫"对齐数"的数字的整倍数的地址。

(对齐数的概念)

  • 在不同的编译器中,默认的对齐数也有所不同,Visual Studio Code的编译器的对齐数是8,Linux的编译器的对齐数是4。
  • 在结构体存址时,对齐数取(编译器默认的对齐数)和(该成员变量大小)中较小的数。

结构体所占内存大小等于最大对齐数(结构体中每个成员变量都有一个对齐数各成员变量中最大的对齐数)的整数倍

如果结构体有嵌套,那么嵌套的结构体存储在自己成员的最大对齐数的整数倍地址处,嵌套结构体大小为所有成员(包括嵌套的结构体的成员)的最大对齐数的整数倍

② 结构体内存对齐数的运算

通过这一段定义大家应该能够大概了解结构体内存对齐的基本概念了,但过于笼统的概念读起来或许会让大家有点难以理解,对于其中的对齐数,或许大家还带有一些疑虑和困惑,那么接下来我给大家具体的讲解一下对齐数到底应该如何计算,和结构体存储到底怎么样才能避免浪费空间

比如此时我们创建一个结构体变量:

struct Stu1 {
	char name;
	int id;
	char sex;
};

然后让我们来分析一下将这个结构体大小该如何计算:

我们先将结构体变量中定义的第一个成员变量存放,存放后样式应该是这样的。

在这一步还看不出什么,那么我们紧接着将第二个成员变量也计算进去,看看现在的样式:

我们发现结构体的大小一下子从1变成了8,并且其中出现了三个浪费的空间,那么造成这种情况的原因是什么呢?让我们来一起计算一下它现在的对齐数

我们知道,结构体中每一个成员变量都有对应的对齐数当结构体Stu1只存放一个char name时char name的对齐数为1,VS编译器默认的对齐数为8,而结构体存址时,对齐数取(编译器默认的对齐数)和(该成员变量大小)中较小的数也就是说此时结构体存址,对齐数取1

而结构体大小又要是各成员变量对应对齐数的最大对齐数的整数倍也就是说此时结构体大小应该为1的整数倍。所以此时我们计算结构体的大小时,打印出的是1。而之后我们的结构体中又多了一个int id的元素,当int id作为第二个成员变量进入结构体时,需要对int id再进行一次对齐数的计算那么此时int型变量对齐数为4同样取(编译器默认的对齐数)和(该成员变量大小)中较小的数,此时结构体存址,对齐数取4

如果按照正常运算,在这里应该大小为4+1=5。但结构体大小必须为各成员变量对应对齐数的最大对齐数的整数倍,此时结构体大小必须为4的整数倍。所以我们只能浪费三个空间最终占用8个空间接下来我们再将char sex加入结构体中,让我们一起猜一下现在结构体的大小会是多大呢

没错,现在结构体的大小就是12啦,因为现在对齐数已经变为4了,所以还需要再浪费3个空间,让大小变为对齐数的整数倍~

那么接下来让我们来探究一下结构体发生嵌套时,大小应该如何计算吧~(上面的对齐规则中提到过)

struct Stu1 {
	char name;
	char sex;
	int age;
};
struct Stu2
{
	char name1;
	struct Stu1 s1;
	char age;
};

我们先来计算一下结构体变量Stu1的大小:因为前两个char型变量的存入不会改变各成员变量中最大对齐数所以可以连续存放而等存入int型变量时,各成员变量中最大对齐数就变成了4于是结构体大小就必须要被4整除,所以浪费两个空间,结构体大小为8。

然后我们再来探究一下结构体嵌套情况下,结构体的大小Stu2的计算由于计算嵌套结构体中最大对齐数时,需要将被嵌套的结构体中的成员变量也列入其中所以虽然Stu2中除了结构体Stu1 s1以外只有大小为1的char型变量但最大对齐数仍需要算上Stu1 s1中的int型,对齐数从而变成了4使得两个char型变量都浪费了3个空间

③ 结构体内存对齐存在原因

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

性能原因:原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问就足够了。或许光这样说,大家还是会听的云里雾里,不明白为什么有时候需要两次内存访问,而有时候仅访问一次就够了。那么接下来我用作图的方式为大家详细的讲解一下:

假设我们在结构体中创建一个char aint b两种变量,分别用结构体内存未对齐结构体内存对齐的两种存放地址的方法,来查看处理器访问内存的情况

假如我们的处理器现在一次会读取四个字节

当处理器进行读取时,读取a后,对于b的读取只读取了一半,如果想将a完全读取,就需要再读取一次,于是就进行了两次访问。(因为假设一个处理器总是从内存中取4个字节,如果我们没有将所有数据对齐存放,我们可能需要执型两次内存访问,因为对象可能被分放在两个4字节内存块中。)

而内存对齐时,两个数据处于对齐的状态,都均匀的分布在各自的4字节内存块中而处理器每次进行访问都访问四个字节,所以访问时并不需要对b分次访问两次,而是访问一次就足够了

总而言之结构体的内存对齐是拿空间来换取时间。 

好啦,那么关于上篇讲解内存函数的文章中所提到的结构体内存对齐的知识就为大家分享到这里啦,如果讲解上有什么问题,或者哪里出现了错误,还请大家多多指出,我也会虚心学习的!我们下一期再见啦ヾ( ̄▽ ̄)Bye~Bye~

;