结构体内存对齐

结构体内存对齐

:本文的编程环境是visual studio2019;64位win10系统

一、什么是结构体内存对齐?

我们先来观察这样一段代码:

#include<stdio.h>

typedef struct s1{
	char c1;
	char c2;
	int i;
};
typedef struct s2{
	char c1;
	int i;
	char c2;
};

int main() {

	printf("%d\n",sizeof(struct s1));
	printf("%d\n",sizeof(struct s2));
	return 0;
}

我们看一下输出结果:

image-20240325110029593

我们可以看到,两个结构体s1和s2内部的数据都是两个char类型和一个int类型数据,只是存放的顺序不同,其结构体整体的大小竟然发生了改变。这就是结构体内存对齐。

定义:

结构体内存对齐是指创建结构体变量时,编译器会根据特定规则把内存会按照特定的规则分配空间以存储结构体的成员,以提高内存访问效率和性能。


二、结构体内部 内存布局的观察

我们在一个代码案例中看到编译器输出的结构是8 / 12;但是我们知道char类型的内存大小位一个字节,int类型的内存大小位4个字节;为什么S1的内存大小是8个字节而不是6个字节呢?

为了进一步的深入研究结构体成员变量在结构体内的内存分布,我们引入了offsetof:

offsetof 是C语言中的一个宏,用于计算结构体中成员变量的偏移量(offset)。它的作用是返回指定结构体中特定成员变量的偏移量,即该成员相对于结构体起始地址的偏移量。

offsetof的使用需要引入头文件:#include <stddef.h>

cplusplus官网介绍:

image-20240325111814233

image-20240325114032413

我们对案例一进行测试:

#include<stdio.h>
#include <stddef.h>

typedef struct s1{
	char c1;
	char c2;
	int i;
};
typedef struct s2{
	char c1;
	int i;
	char c2;
};

int main() {
		
	printf("s1的内存大小:%d\n",sizeof(struct s1));
	printf("第一个成员变量与起始点的偏移量:%d\n",offsetof(struct s1,c1));
	printf("第二个成员变量与起始点的偏移量:%d\n",offsetof(struct s1,c2));
	printf("第三个成员变量与起始点的偏移值:%d\n",offsetof(struct s1,i));

	printf("s1的内存大小:%d\n", sizeof(struct s2));
	printf("第一个成员变量与起始点的偏移量:%d\n", offsetof(struct s2, c1));
	printf("第二个成员变量与起始点的偏移量:%d\n", offsetof(struct s2, i));
	printf("第三个成员变量与起始点的偏移量:%d\n", offsetof(struct s2, c2));
	return 0;
}

输出结果如下:

image-20240325114515405

那么这里的内存分布大概是什么样子呢?

image-20240325120144590

结构体s1:

c1的的偏移量为0,则c1的地址就是从s1的起始地址开始,占一个字节;

c1的的偏移量为1,则c1的地址就是从s1的起始地址后一个字节开始,占一个字节;

i的的偏移量为4,则c1的地址就是从s1的起始地址后4个字节开始,占4个字节;

结构体s2同理;

我们会发现,图片中还有一些空余的地址空间(白色区域)没有被使用,这就是被浪费掉的地址空间。

这里就解释了,为什么S1的内存大小是8个字节而不是6个字节因为结构体的内存分配中存在未被使用的地址空间。


三、内存对齐方式

我们虽然通过测试,明白了案例一的内存空间分配情况。但是我们还是不知道为什么编译器会这样分配内存空间。

下面我介绍一下结构体内存对齐的规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到对齐数的整数倍的地址处。

    对齐数 = 编译器默认的一个对齐数 与 当前成员大小的较小值(所以一般情况下也是成员大小)

  • 本文使用的visual studio的默认对齐数为8
  • gcc编译器无默认对齐数,对齐数是自身成员变量的大小
  1. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

那我们现在再来回顾案例一:

s1结构体:

c1是第一个成员,在与结构体变量偏移量为0的地址处。

c2要对齐,本身大小为1,对齐数为8;所以对齐到1的整数倍的地址,即为地址1;

i也要对齐,本身大小为4,对齐数为8;所以对齐到4的整数倍的地址,即为地址4;

s2结构体:

c1是第一个成员,在与结构体变量偏移量为0的地址处。

i本身大小为4,对齐数为8;所以对齐到4的整数倍的地址,即为地址4;

c2本身大小为1,对齐数为8;所以对齐到1的整数倍的地址,,但是0~7的地址空间被占用,所以c2起始地址为8;

那么我们再来看一个结构体:

typedef struct s3 {
	char i;
	double d;
	int c1;
};

我们来判断一下他的结构体大小;

这里可能会有些人以为是20;其实正确的结果是24。

考虑第三个规则:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

image-20240325122613071


四、为什么要引入结构体内存对齐?

  1. 硬件要求(兼容性):不是所有平台都能任意的访问地址上的任意数据;

    通过结构体内存对齐可以确保不同平台、不同编译器下的结构体布局一致,增加程序的可移植性和兼容性。

  2. 提高访问速度

    对齐后的数据可以直接通过单个 CPU 指令来读取,而非对每个字节逐个访问,从而提高内存访问速度。(以空间换时间)

    如在32位的操作系统上,字长为32位,即一次可以读取32位的数据信息。

    如果没有内存对齐,那么这一次读取,可能只读取到一个数据信息的一半(如下图所示);有内存对齐后就不会发生这种情况了。

    image-20240325123753507

所以在设计结构体的时候,我们让占用空间小的成员尽量集中在一起。 既满足内存对齐,又节省空间

*五、修改默认对齐数

我们在对齐规则中,我们知道visual studio的默认对齐数是8,但是gcc编译器(Linux)无默认对齐数。

那么我们可不可以对默认对齐数进行修改呢?

# pragma预处理指令

我们可以通过

#pragma pack(1)

来使得默认对齐数为1

注:

  • #pragma 是编译器相关的指令,不属于 C 语言标准的一部分,因此在不同的编译器中可能会有不同的行为。
  • 虽然 #pragma 的使用可以提供一些便利,但过度依赖它可能导致代码的可移植性变差,因为不同的编译器对 #pragma 的支持程度不同。
  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值