C 语言编程 — 内存对齐

目录

内存对齐

计算机的内存空间都是按照字节划分的,元素(包括:变量、结构体成员、共用体成员)会按照定义的顺序一个一个放到内存中。从理论上讲,似乎可以从任意地址开始存储任何类型的元素,但实际上元素之间并不是紧密排列的。因为计算机系统对于基本数据类型在内存中的存放位置是有限制的:假设一个变量占用 n 个字节,则该变量的起始地址必须能够被 n 整除(起始地址 % n = 0)。

例 1:

#include<stdio.h>

struct {
    char x;
    int y;
    char z;
} Test;

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

执行结果为:12。

下图为上例的内存布局图,有以下几个关键点:

  • 结构体成员 x(红色)、y(蓝色)、z(绿色)循序排列。
  • 结构体成员之间并不紧密排列。
  • 成员 y 的大小为 4B,所以对齐系数为 4,即便 x、z 的大小为 1B,但仍然严格按照对齐系数进行布局。
    在这里插入图片描述

例 2:

#include<stdio.h>

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

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

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

int main()
{
    printf("%d\n", sizeof(Test1));  // 输出 8
    printf("%d\n", sizeof(Test2));  // 输出 12
    printf("%d\n", sizeof(Test3));  // 输出 8
    return 0;
}

3 个结构体变量的内存布局:
在这里插入图片描述

可见,从存储结构体的空间首地址开始,每个成员被放置到内存中时,都会认为内存是按照自己的大小来进行划分的,因此成员的起始地址一定会在自身宽度的整数倍上,这就是所谓的内存对齐。

对于结构体而言,结构体成员的起始地址必须能够被成员中所占空间值最大的那个整除,例如:上述例子中的 int y。所以,通常结构体中成员变量声明的顺序是按照成员类型大小 从小到大 的顺序进行,有时候这样可以减少中间的填充空间。

下图为不准守 从小到大 顺序原则的结果:

在这里插入图片描述

为什么要内存对齐?

内存对齐作为一种强制的要求,有几点原因:

  1. 简化处理器与内存之间传输系统的设计

  2. 平台移植性:不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些特定地址处取某些特定的数据,否则就会抛出硬件异常。也就是说在计算机在内存读取数据时,只能在规定的地址处读数据,而不是内存中任意地址都是可以读取的。

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

假如没有内存对齐机制,数据可以任意存放,现在一个 int 变量存放在从地址 1 开始的连续四个字节地址中,该处理器去取数据时,要先从 0 地址开始读取第一个四字节块,剔除不想要的字节(0 地址),然后从地址四开始读取下一个四字节块,同样剔除不要的数据(5,6,7 地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。

有了内存对齐后,int 类型数据只能存放在按照对齐规则的内存中,比如说 0 地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

对于 32 位系统,如下图的 A 可能需要 2 条指令访问,而 B 只需 1 条指令。
在这里插入图片描述

再举 3 个例子:

  1. 成员变量对齐使用
	int a[] = {'abcd', 4444};
	typedef struct _GPIO_t {
		char in;
		char out;
		char type;
		char value;
		int data;
	} GPIO_t;
	
	GPIO_t *GPIOA = (GPIO_t *)&a;

	printf("%c \n", GPIOA->in);
	printf("%c \n", GPIOA->out);
	printf("%c \n", GPIOA->type);
	printf("%c \n", GPIOA->value);
	printf("%d \n", GPIOA->data);

在这里插入图片描述

:数据存储格式分为大小端存储,所以结构引用输出顺序可能不太对应。

  1. 成员变量没有对齐使用
	int a[] = {1234, 5678, 'abcd', 4444};
	typedef struct _GPIO_t {
		int in;
		int out;
		char type;
		char value;
		int data;		// 会自动四字节对齐因此直接指向 a[3] 
	} GPIO_t;
	
	GPIO_t *GPIOA = (GPIO_t *)&a;

	printf("%d \n", GPIOA->in);
	printf("%d \n", GPIOA->out);
	printf("%c \n", GPIOA->type);
	printf("%c \n", GPIOA->value);
	printf("%d \n", GPIOA->data);

在这里插入图片描述

:因为自动对齐缘故,其中有些数据会自动丢掉。

  1. 成员变量不使用自动给对齐
	int a[] = {1234, 5678, 'abcd', 4444};
	
	#pragma pack(1)		// 强制设置 1 字节对齐 
	typedef struct _GPIO_t {
		int in;
		int out;
		char type;
		char value;
		int data;		// 会自动四字节对齐因此直接指向 a[3] 
	} GPIO_t;
	
	GPIO_t *GPIOA = (GPIO_t *)&a;

	printf("%d \n", GPIOA->in);
	printf("%d \n", GPIOA->out);
	printf("%c \n", GPIOA->type);
	printf("%c \n", GPIOA->value);
	printf("%d \n", GPIOA->data);

在这里插入图片描述

注:最后一个成员由于对齐错误出现乱码。

内存对齐跟平台有关

需要注意的是,C 语言基本类型的大小是与平台相关的。所以我们在进行内存对齐设计时,首先应该了解当前平台的情况。

#include<stdio.h>

#define BASE_TYPE_SIZE(t)   printf("%12s : %2d Byte%s\n", #t, sizeof(t), (sizeof(t))>1?"s":"")

void base_type_size(void)
{
    BASE_TYPE_SIZE(void);
    BASE_TYPE_SIZE(char);
    BASE_TYPE_SIZE(short);
    BASE_TYPE_SIZE(int);
    BASE_TYPE_SIZE(long);
    BASE_TYPE_SIZE(long long);
    BASE_TYPE_SIZE(float);
    BASE_TYPE_SIZE(double);
    BASE_TYPE_SIZE(long double);
    BASE_TYPE_SIZE(void*);
    BASE_TYPE_SIZE(char*);
    BASE_TYPE_SIZE(int*);

    typedef struct {
    } StructNull;

    BASE_TYPE_SIZE(StructNull);
    BASE_TYPE_SIZE(StructNull*);
}


int main()
{
    base_type_size();
    return 0;
}

执行结果:

        void :  1 Byte
        char :  1 Byte
       short :  2 Bytes
         int :  4 Bytes
        long :  8 Bytes
   long long :  8 Bytes
       float :  4 Bytes
      double :  8 Bytes
 long double : 16 Bytes
       void* :  8 Bytes
       char* :  8 Bytes
        int* :  8 Bytes
  StructNull :  0 Byte
 StructNull* :  8 Bytes

对齐系数

每个特定平台上的编译器都有自己默认的对齐系数。

在这里插入图片描述

使用 pragma 宏指令修改对齐系数

对齐系数是可以改变的,通过预处理器指令 #pragma pack(n),n=1,2,4,8,16 来自定义对齐系数,通过 #pragma pack(),来取消自定义对齐系数。


#pragma pack(1)

typedef struct  {
    char e_char;
    long double e_ld;
} S14;

#pragma pack()

宏定义 pragma pack(value) 的 value 就是指定的对齐值,通常 value 的值取 2 的较小次方。

  • 如果 value 的值小于变量类型的对齐值,则按照 value 的值进行对齐。
  • 如果 value 的值大于变量类型的对齐值,则按照原来的对齐值进行对齐。

简而言之,使用该宏的时候,按照 value 值和原来对齐值之间较小的值进行对齐。

对于上例子的三个结构体,如果前面加上 #pragma pack(1),那么此时有效对齐值为1字节,此时根据对齐规则,不难看出成员是连续存放的,三个结构体的大小都是 6 字节。

在这里插入图片描述

如果前面加上 #pragma pack(2),有效对齐值为 2 字节,此时根据对齐规则,三个结构体的大小应为 6,8,6。内存分布图如下:

在这里插入图片描述

另外,还有如下的一种修改方式:

  • __attribute((aligned (n))),让所作用的结构成员对齐在 n 字节自然边界上。如果结构中有成员的长度大于 n,则按照最大成员的长度来对齐。
  • attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

内存对齐的原则

  1. 结构体成员按自身数据类型的对齐系数进行对齐:第一个结构体成员放在 offset(偏移量)为 0 的地方,后续每个成员的起始地址要从该成员大小的整数倍开始。

  2. 结构体中成员为其他结构体,则结构体成员要按自身结构体内部的最大对齐值(成员中的数据类型所占空间值最大的那个)进行对齐:比如 struct A 中包含 struct B 类型的成员,B 中有 char、int、double 元素,那么 B 应该从 sizeof(double) 的整数倍开始存储。

  3. 结构体的自身对齐值是其成员中自身对齐值最大的那个值:即:结构体的总大小必须是其内部最大成员的整数倍,不足的要补齐

下面通过几个例子,加深对这几个原则的理解(32 位系统):

EXAMLE1: sizeof(A) = 8 字节,因为两个成员变量 int 都是 4 字节;sizeof(B)=8 字节,因为原则 1.,如果将 b1 和 b2 的位置互换,则准守原则 3.。

struct A {
  int a1;	// 4B
  int a2;	// 4B
};

struct B {
  char b1;	// 1B
  int b2;	// 4B
};

EXAMLE2:sizeof(C) = 8 字节,而拥有相同成员定义的 D 则为 sizeof(D)=12 字节。C 和 D 的区别在于两者定义的成员的顺序不一样,这导致了两者占用内存的大小也不一样。因为 C 按原则 1. 进行对齐后,刚好满足原则 3.;而 D 中将 char 放在最后,按原则 1. 进行对齐后,并不满足原则 3.,还需要按照原则 3. 进行补齐,所以占用了额外的空间。

struct C {
  short c1;		// 2B
  char c2;		// 1B
  long c3;		// 4B
};

struct D {
  short d1;		// 2B
  long d2;		// 4B
  char d3;		// 1B
};

EXAMPLE3:sizeof(E) = 16 字节,因为原则 2. 要求成员结构体 e2,需要按照 C 的对齐值 4 对齐,所以内存从 offset(偏移值)为 4 的地方开始存储。将 e2 和 e3 互换位置,可以得到 sizeof(E)=12 字节。

struct E {
  char e1;		// 1B
  struct C e2;	// 8B
  short e3;		// 2B
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__0809 返回首页
实付 49.00元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值