结构体内存对齐良心讲解

本文将介绍以下内容:

一、什么是内存对齐?

二、结构体大小如何计算?

三、为什么存在内存对齐?

四、如何修改默认对齐数?

五、笔试题练习


正文

一、什么是内存对齐?

  首先,内存对齐是属于编译器的“管辖”范围,编译器为程序中的每个数据单元安排在适当的位置上。但是在 c 语言中我们是被允许干预内存对齐的,也就是说我们可以一定程度上的控制如何内存对齐。
内存对齐是为了硬件计算起来效率更高而设计的
  我们通过一些例子来理解下什么是内存对齐:(以下程序皆在vs2013下运行)
例1:

typedef struct s1
{
	char a;
	int b;
	char c;
} s1;
printf("%d\n", sizeof(s1));

运行结果是12。

typedef struct s1
{
	char a;
	char b;
	char c;
} s1;
printf("%d\n", sizeof(s1));

运行结果是3。

typedef struct s1
{
	char a;
	short b;
	char c;
} s1;
printf("%d\n", sizeof(s1));

运行结果是6。

typedef struct s1
{
	char a;
	double b;
	char c;
} s1;
printf("%d\n", sizeof(s1));

运行结果是24。


从例1直观的来看,感觉好像是低字节的元素在向高字节对齐。我们继续往下看!!!


例2:

typedef struct s2
{
	char a;
	char b;
	int c;
} s2;
printf("%d\n", sizeof(s2));

运行结果是8。

例3:

typedef struct s3
{
	double d;
	char c;
	int i;
} s3;
printf("%d\n", sizeof(s3));

运行结果是16。


例 2 应该是将两个 char 凑成一个4字节的 int,所以计算出来是8。
例 3 应该是将 char 和 int 凑成了一个 8字节的double,所以计算出来是16。
这看起来也是往低字节往高字节的元素对齐。那么看下一个例子!!


例4:

	typedef struct s3
	{
		double d;
		char c;
		int i;
	} s3;
	
	typedef struct s4
	{
		char c1;
		struct s3 s3;
		double d;
	} s4;
	printf("%d\n", sizeof(s4));

运行结果是32。


注意:这里结果发生了变化,如果按照我们上面的推论,那么这个计算出来应该是往 s3 这个结构体上对齐,s3 是16个字节,那么按理应该是 48 才对,看起来这并不是无休止的往最大的字节元素对齐。那么我们就来看看结构体的大小到底应该如何计算?


二、结构体大小如何计算?

  我们先来看看结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移为 0 的地址处。
  2. 其他成员变量要对齐到对齐数的整数倍
      对齐数 = 编译器默认的一个对齐数 与 该成员中最大的变量 两者之间较小的那个
      提一下:vs中默认的对齐数是 8
          Linux下默认的对齐数是 4
  3. 结构体总大小为最大对齐数(每一个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

看了这些规则,我们来一条条分析:
  第一条很好理解,简单来说,结构体第一个成员的地址和结构体的地址是一样的。但是意义不一样,第一个成员的地址是给结构体的地址加上一个 0 得到的就是第一个成员的地址。
  主要是第二条看起来比较绕口,但是仔细分析也是很好理解,这时候可以看我们的例子,在例2中,结构体中最大的成员是 int ,而vs默认的对齐数是8,我们选两者中较小的一个作为对齐数,那么就是取 4 作为对齐数。
  再看例 3 中,结构体成员最大的是 double 有 8 个字节,而vs的默认对齐数也是 8 ,所以取 8 作为对齐数。
  再看例 4 中,结构体成员中最大的是结构体 s3 ,是 16 个字节,而vs默认对齐数是8,取两者较小的作为对齐数,所以就取的是 8 作为对齐数,s3 的大小还是 16 个字节,其他成员变量向 8 对齐。

三、为什么存在内存对齐?

  大部分的参考资料都是这样解释的:
1、平台原因(移值原因):
  不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取特定类型的数据,否则抛出硬件异常。
2、性能原因:
  数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
  原因在于,为了访问未对齐的内存,处理器需要作出两次内存访问,而对齐的内存仅需要一次访问,也就是下面的解释。
  cpu在取数据的时候一般都是 4 个字节或 8 个字节的取,如果没有内存对齐这回事那么取数据就会出现下面的情况
在这里插入图片描述
没有内存对齐的话,取出来的数据就是一个 char 和 3/4 个 int,不是一个完整的 int,为了取出一个完整的 int 还得再取一次,这样就很僵硬了。
  但是当我们把 char 补成四个字节,那么每次就可以取出来完整的数据了,cpu 再去解析也就方便了很多。所以说内存对齐是为了硬件计算起来效率更高设计的
在这里插入图片描述

总的来说,结构体的内存对齐是用 空间 换 时间 的做法

四、如何修改默认对齐数?

  借助预处理指令 #pragma pack()
#pragma pack(2) :将默认对齐数修改为2。默认对齐数一旦修改,那么结构体计算的大小也就发生了相应的变化。
#pragma pack() :取消设置的默认对齐数,还原为默认对齐数。

五、笔试题练习

提问:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明?
答:

typedef struct s1
{
	char a;
	int b;
	char c;
} s1;

#define OFFSET(a, b) ((char*)(&b) - (char*)(&a))

int main()
{
	s1 s;
	printf("成员 b 相对于首地址的偏移量为:%d\n", OFFSET(s, s.b));
	system("pause");
	return 0;
}

如果直接用两个指针进行相减,减出来肯定有问题,因为这里的指针是带类型的,所以我们需要将指针强转为char*进行相减,那么计算出来的就是字节数。
在这里插入图片描述


以上就是内存对齐的相关知识!!!

猜你喜欢

转载自blog.csdn.net/Code_beeps/article/details/89199411