结构体的内存对齐是有关结构体内容的很重要一个知识点,主要考察方式是计算结构体的字节大小。
当我们对计算结构体一无所知,我们不妨自己思索如何计算,是不是直接计算结构体成员变量占用内存的大小呢?
那我们先举个例子
struct s1
{
int i;
char a;
char b;
};
struct s2
{
char a;
int i;
char b;
};
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
观察发现结构体的大小计算跟我们想的很不一样。
不应该是两个char类型,一个int类型,2*1+4答案不应该是6吗?
上面两个结构体内容是一样的,只有顺序不一样,为何计算结果不一样呢?
我们就带着以上的疑问去探索!
我们要研究明白结构体的成员列表在内存中到底是如何存储的,首先要知道结构体的各个成员变量在内存中相较于起始位置的偏移量。这时候要引用到offsetof,这个宏可以计算结构体成员相较于结构体起始位置的偏移量。
如何使用宏offsetof?
首先有头文件:#include
参数是类型,和成员名,返回值就是结构体成员相较于结构体起始位置的偏移量。
我们先试着打印下s2各个成员关于结构体起始位置的偏移量。
发现结果是0、4、8,我们可以画一张内存图进行理解。
如图所示,根据offsetof我们可以得到这样的内存存储模式,但是这样一共也就9个字节,后面的3个字节从何而来?中间多出来的3个字节又从何而来?
我们继续探索。
结构体到底如何计算?
我们经过上面的分析,发现结构体成员不是按照顺序在内存中连续存放的,而是有一定的对齐规则,接下来我们就研究结构体的内存规则。
我们首先要知道结构体变量成员的自身字节大小,然后去寻找对齐数,对齐数的寻找方法就是将自身字节大小和默认对齐数比较,取较小值,这样先找到对齐数,然后根据自身的字节大小去填充,就完成了成员在内存中的存储,最后在所有的成员已经结束存储,再计算最大对齐数(所有成员的对齐数中最大值),这样就完成了计算!
我们既然已经知道规则和计算方法,就让我们小试牛刀一下~
struct s3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct s3));
return 0;
}
上面图片的写法就是左边是本身成员变量的字节大小,右边是默认对齐数进行比较,最后再从对齐数中找出最大值,就是最大对齐数,所以最后0~15就是存储结构体的大小,也就是一共16个字节
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;
}
上面是嵌套结构体场景,结构体S3本身大小是16,需要对齐到自身最大对齐数的位置,也就是8,然后double类型的对齐数是8,最后总字节大小也满足最大对齐数,所以一共32个字节。
不是所有的硬件平台都能访问任意地址上的任意数据;某些平台只能在某些地址处取某些地址处取特定类型的数据,否则抛出硬件异常
数据结构(尤其是栈)应该尽可能在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说
结构体的内存对齐,就是让空间换时间。
我们在设计结构体时,可以人为的节省空间——让占用空间小的成员尽量集中在一起。
例如我们之前举的例子,尽管两个结构体存的成员变量一样,但是顺序不一样,结构体内存大小也是不同。
对,你没有听错,默认对齐数是可以修改滴,当我们把默认对齐数修改为1时,结构体的成员变量就是连续存储的。代码如下,计算出来的大小就是4+1+8=13
#pragma pack(1)//修改默认对齐数为1
struct s
{
int a;
char b;
double c;
};
#pragma pack()//修改默认对齐数为默认
int main()
{
printf("%d\n", sizeof(struct s));
return 0;
}