问题:根据内存对齐,讲一下怎么获得结构成员相对于结构开头的字节偏移量
查资料:
【C/C++】内存对齐(超详细,看这一篇就够了)_c++内存对齐-CSDN博客
8大基本数据类型各占多少字节和一些单位常识_各变量类型占多少字节-CSDN博客
什么是内存对齐
第一个问题:什么是内存对齐?为什么要内存对齐?
-
内存对齐(Memory Alignment)是计算机科学中的一个概念,指的是将数据存储在特定的内存地址上,以提高系统性能。
-
内存对齐的主要目的是使数据访问更快、更有效率以及更好的缓存优化。
-
内存对齐的根本原因:对当前因地址总线限制而导致的内存寻址“不自由”的一种“妥协”。
例如:对于32位操作系统(cpu一次取32位,也就是4个字节的数据):
寄存器只能从整除以4的地址开始读取数据,因此内存对齐能够在一定程度上减少读取数据的次数。
相关的基本概念主要有:
- 内存地址:计算机内存是由一系列连续的地址组成的,每个地址对应一个字节。
- 对齐要求:特定类型的数据通常要求存储在满足某种条件的地址上,这个条件称为对齐要求。例如,32位整数通常要求存储在能被4整除的地址上。
- 对齐边界:对齐边界是指数据类型要求的内存地址必须是某个特定数值的倍数。例如,32位数据类型的对齐边界是4字节。
对齐的好处:
- 性能提升:正确的内存对齐可以使CPU在读取或写入数据时更加高效,因为大多数现代CPU对齐访问比非对齐访问要快。
- 硬件限制:一些硬件架构要求对齐访问,否则会引发异常或性能严重下降。
内存对齐示例
先写一段代码看看
#include <cstdio>
struct Test
{
int a; // 整型 4
char b; // 字符型 1
int c; // 整型 4
double d; // 双精度浮点型 8
} test;
int main() {
test.a = 10; // %d: 10
test.b = 'A'; // %c: A
test.c = 20; // %d: 20
test.d = 3.14; // %f: 3.140000
printf("test size: %d\n", sizeof(test));
return 0;
}
打印结果:
test size: 24
- 表格中的char这里有点问题,通常情况下是占1个字节的,当表示Unicode字符时占2个字节。
根据8大基本数据类型所占字节数查询得到:4 + 1 + 4 + 8 = 17,但通过sizeof打印占用空间结果却是24个字节,这是为什么呢?
其实是因为编译器和硬件对结构体内的数据进行了内存对齐。
- 结构体是用户定义的复合数据类型,可以包含多个不同类型的成员。
- 在C/C++中, 结构体占用的空间是连续的,成员在内存中按声明顺序排列的,但可能会因为对齐要求而插入填充字节。这样实际的占用空间就比数据所占的要大。
- 内存对齐会增加数据结构的内存占用,因为可能需要插入额外的填充字节。例如,在32位系统中,一个包含
char
和int
的结构体,如果不对齐占用5字节,但如果对齐则可能占用8字节。 - 对齐规则看后面的详解
这里代码中的结果实际内存布局如下:
int a
:4字节对齐char b
:1字节对齐- 填充 (padding) (3 bytes):为了对齐下一个
int
成员c
,在char b
后面插入3字节填充 int c
:4字节对齐- 填充 (padding) (4 bytes):为了对齐下一个
double
成员d
,在int c
后面插入4字节填充 double d
:8字节对齐
因此是 4 + 1 + 3 + 4 + 4 + 8 = 24个字节
内存对齐规则
说的什么玩意儿,生怕我看明白了是吧,下面是逐行解析:
标准对齐规则
-
内存分配顺序:结构体的成员变量在内存中的排列顺序与它们在结构体定义中的声明顺序一致。
-
偏移量对齐:变量的偏移量是其类型大小的整数倍。例如:
struct Test { int a; // 4 bytes char b; // 1 byte int c; // 4 bytes double d; // 8 bytes }; 偏移量和对齐情况如下: a 从偏移量0开始,占4字节。 b 从偏移量4开始,占1字节。 c 需要4字节对齐,因此从偏移量8开始,占4字节(插入3字节填充)。 d 需要8字节对齐,因此从偏移量16开始,占8字节(插入4字节填充)。
-
整体对齐:确保结构体的总大小对齐到其成员中最大类型的大小。例如,
struct Test
中最大类型是double
,大小为8字节,因此整个结构体的大小必须是8的整数倍。咱们是24刚好合适,如果不合适就要接着padding了
使用 #pragma pack(n)
的对齐规则
#pragma pack(n)
指令可以控制对齐方式,以适应特定需求。n 是一个字节数,表示对齐边界。使用这个指令后,对齐规则变为:
-
变量对齐:每个变量的偏移量必须是
n
和变量大小中较小值的整数倍。例如,如果n
为2,而变量大小为4,则偏移量必须是2的整数倍。 -
整体对齐:结构体的总大小必须是
n
和最大变量大小中较小值的整数倍。例如,如果n
为2,而结构体中的最大类型大小为8,则结构体的总大小必须是2的整数倍。 -
n 的取值:n 必须是2的幂。如果 n 不是2的幂,编译器将使用默认对齐规则。
-
一个例子帮助理解:
#include <cstdio> #pragma pack(2) struct Test { int a; // 整型 char b; // 字符型 int c; // 整型 double d; // 双精度浮点型 }; #pragma pack() int main() { printf("test size: %d\n", sizeof(Test)); return 0; } 打印结果:test size: 18 int a 从偏移量0开始,占4字节(对齐到2字节)。 char b 从偏移量4开始,占1字节。 int c 需要2字节对齐,因此从偏移量6开始,占4字节。 double d 需要2字节对齐,因此从偏移量10开始,占8字节。
最终布局:所以整体占了18个字节
总结:
-
内存对齐是由编译器、硬件、操作系统以及编程语言的特性共同实现的。编译器负责生成对齐的数据结构和代码,硬件提供对齐访问的性能优势,操作系统管理内存分配时考虑对齐要求,而编程语言提供控制对齐的工具和指令。这些共同作用确保程序在执行时具有高效的内存访问和最佳的性能。
-
可以看一下内存对齐和缓存行之间的联系(我懒得看了)
-
内存对齐多少字节其实是可以手动指定的,例如:
struct Example { char a; int b; short c; } __attribute__((aligned(4))); // GCC中的对齐指令,指定整个结构体按照4字节对齐。
__declspec(align(8)) struct Test { int a; char b; int c; double d; }; // MSVC编译器中,对齐为8字节
计算结构成员相对于结构开头的字节偏移量
关于阿秀cpp基础语法第70条的解析如下:
源代码:
#include <iostream>
#include <stddef.h>
using namespace std;
struct S
{
int x;
char y;
int z;
double a;
};
int main()
{
cout << offsetof(S, x) << endl; // 0
cout << offsetof(S, y) << endl; // 4
cout << offsetof(S, z) << endl; // 8
cout << offsetof(S, a) << endl; // 12
printf("test size: %d\n", sizeof(S));
return 0;
}
打印结果:
0 // 偏移量从0开始,占4字节
4 // 偏移量从4开始,占1字节
8 // 需要4字节对齐,因此从偏移量8开始,占4字节。
16 // 需要8字节对齐,因此从偏移量16开始,占8字节。
test size: 24 // 整体占用16+8=24个字节的空间
加上#pragma pack(4)指定4字节对齐方式后
#include <iostream>
using namespace std;
#pragma pack(4)
struct S
{
int x;
char y;
int z;
double a;
};
#pragma pack()
int main()
{
cout << offsetof(S, x) << endl; // 0
cout << offsetof(S, y) << endl; // 4
cout << offsetof(S, z) << endl; // 8
cout << offsetof(S, a) << endl; // 12
printf("test size: %d\n", sizeof(S));
}
打印结果:
0 // 偏移量从0开始,占4字节
4 // 偏移量从4开始,占1字节
8 // 需要4字节对齐,因此从偏移量8开始,占4字节
12 // 需要min(8, 4)字节对齐,因此从偏移量12开始,占8字节。
test size: 20 // 整体占用12+8=20个字节的空间
检查是否整体对齐:n = 4,最大类型大小为8,结构体总大小为20,是min(4, 8)的整数倍
搞完了。。