什么是内存对齐?
一句话概括:一个结构体变量定义完之后,其在内存中的存储并不等于其所包含成员的大小之和。
现代计算机中内存空间都是按照字节(byte)进行划分的,所以从理论上讲对于任何类型的变量访问都可以从任意地址开始,但是在实际情况中,在访问特定类型变量的时候经常在特定的内存地址访问,所以这就需要把各种类型数据按照一定的规则在空间上排列,而不是按照顺序一个接一个的排放,这种就称为内存对齐。
几个基本概念
对齐值
(1)基本数据类型自身对齐值:基本数据类型的自身所占空间大小。
(2)指定对齐值:使用#pragam pack(value)时,指定的对齐值value。
(3)结构体或类的自身对齐值:包含成员中对齐值最大的那个值。
有效对齐值
有效对齐值:是给定值#pragma pack(n) 与 结构体中最长数据类型长度,两者中较小的那个。
偏移量offset
偏移量可以简单的理解为:对象地址与成员变量地址之间的距离。具体为,结构体第一个成员的偏移量是0(起始点与数据本身开始点是同一个点),以后每个成员相对于结构体首地址的偏移量都是 该成员大小与有效对齐值中较小那个 的整数倍,如有需要编译器会在成员之间加上填充字节。
计算总大小
也就是sizeof的值。结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
默认对齐规则
1.基本数据成员对齐
如果结构体只包含基本类型的成员。
- 对于结构体的各个成员,第一个成员的偏移量(offset)为
0,也就是说
第一个成员总是存储在结构体变量开辟的空间的起始地址。
- 其他成员相对于结构体首地址的
offset,
是该成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
2. 结构体成员对齐
- 如果包含了结构体成员,则结构体成员的起始位置为子结构体有效对齐值的整数倍。
- 嵌套结构体大小为所有成员(包括嵌套的结构体的成员)的最大对齐数的整数倍。
3. 结构体总大小对齐
除了结构成员需要对齐,结构本身也需要对齐。结构体的总大小,也就是sizeof结构体的值,必须要是其内部最大成员对齐值(或者说自身对齐值)的整数倍,不足的要补齐。
示例
以下示例测试环境:
Ubuntu 22.04.3 LTS + GCC 11.4.0 x86_64 64 位环境。
有效对齐值为1
struct A1 {
char a;
};
struct A12 {
char a;
bool b;
};
struct A13 {
char a;
bool b;
bool c;
};
void align1() {
std::cout << "sizeof A1: " << sizeof(A) << std::endl;
std::cout << "sizeof A12: " << sizeof(A12) << std::endl;
std::cout << "sizeof A13: " << sizeof(A13) << std::endl;
}
输出结果
sizeof A1: 1
sizeof A12: 2
sizeof A13: 3
有效对齐值为2
struct A21 {
short a;
};
struct A22 {
short a;
bool b;
};
struct A23 {
bool b;
short a;
};
struct A24 {
bool b;
short a;
bool c;
};
void align2() {
std::cout << "sizeof A21: " << sizeof(A21) << std::endl;
std::cout << "sizeof A22: " << sizeof(A22) << std::endl;
std::cout << "sizeof A23: " << sizeof(A23) << std::endl;
std::cout << "sizeof A24: " << sizeof(A24) << std::endl;
}
输出
sizeof A21: 2
sizeof A22: 4
sizeof A23: 4
sizeof A24: 6
内存示意图
有效对齐值为4
struct A41 {
int a;
};
struct A42 {
int a;
bool b;
};
struct A43 {
int a;
bool b;
short c;
};
struct A44 {
bool a;
int b;
short c;
};
struct A45 {
bool a;
int b;
short c;
int d;
};
void align4() {
std::cout << "sizeof A41: " << sizeof(A41) << std::endl;
std::cout << "sizeof A42: " << sizeof(A42) << std::endl;
std::cout << "sizeof A43: " << sizeof(A43) << std::endl;
std::cout << "sizeof A44: " << sizeof(A44) << std::endl;
std::cout << "sizeof A45: " << sizeof(A45) << std::endl;
}
输出
sizeof A41: 4
sizeof A42: 8
sizeof A43: 8
sizeof A44: 12
sizeof A45: 16
内存示意图
有效对齐值为8
struct OL {
long a;
};
struct OIL {
int a;
long b;
};
struct OLL {
long a;
long b;
};
struct OLC {
long a;
char b;
};
struct OLS {
long a;
short b;
};
struct OLI {
long a;
int b;
};
struct OLD {
long a;
double b;
};
void align8() {
std::cout << "sizeof OL: " << sizeof(OL) << std::endl;
std::cout << "sizeof OIL: " << sizeof(OIL) << std::endl;
std::cout << "sizeof OLL: " << sizeof(OLL) << std::endl;
std::cout << "sizeof OLC: " << sizeof(OLC) << std::endl;
std::cout << "sizeof OLS: " << sizeof(OLS) << std::endl;
std::cout << "sizeof OLI: " << sizeof(OLI) << std::endl;
std::cout << "sizeof OLD: " << sizeof(OLD) << std::endl;
}
结果
sizeof OL: 8
sizeof OIL: 16
sizeof OLL: 16
sizeof OLC: 16
sizeof OLS: 16
sizeof OLI: 16
sizeof OLD: 16
内存示意图
带虚函数的类对齐
如果类定义了虚函数,编译器会在对象成员之前插入一个指向虚函数表的指针。在64位系统之中,指针大小为8个字节。
class BaseA {
public:
virtual void Foo() {}
int a;
};
class BaseB {
public:
virtual void Bar() {}
short b;
};
class BaseC {
int c;
};
class ChildAB : public BaseA, public BaseB {
int d;
};
class ChildAC1 : public BaseA, public BaseC {};
class ChildAC2 : public BaseA, public BaseC {
int d;
};
class ChildCA : public BaseC, public BaseA {
int d;
};
void align_virtual() {
std::cout << "sizeof BaseA: " << sizeof(BaseA) << std::endl;
std::cout << "sizeof BaseB: " << sizeof(BaseB) << std::endl;
std::cout << "sizeof ChildAB: " << sizeof(ChildAB) << std::endl;
std::cout << "sizeof ChildAC1: " << sizeof(ChildAC1) << std::endl;
std::cout << "sizeof ChildAC2: " << sizeof(ChildAC2) << std::endl;
std::cout << "sizeof ChildCA: " << sizeof(ChildCA) << std::endl;
}
输出
sizeof BaseA: 16
sizeof BaseB: 16
sizeof ChildAB: 32
sizeof ChildAC1: 16
sizeof ChildAC2: 24
sizeof ChildCA: 24
结构体字段对齐
/*
struct OLS {
long a;
short b;
};
*/
struct ISS {
short ma;
int mb;
OLS mc;
short md;
};
void align_struct() {
ISS vi;
ISS *p = &vi; // vscode调试时候获取地址值方便查看内存
vi.ma = 0x0102;
vi.mb = 0x03040506;
vi.mc.a = 0x0101010101010101;
vi.mc.b = 0;
vi.md = 0x1234;
std::cout << "sizeof ISS - OLS: " << sizeof(vi.mc) << std::endl;
std::cout << "sizeof ISS: " << sizeof(vi) << std::endl;
}
结果
sizeof ISS - OLS: 16
sizeof ISS: 32
结构体成员mc的有效对齐值是8,所以整个ISS的有效对齐值也是8。
调试的时候查看内存
md的位置在mc内存对齐之后的位置。
可以看出结构体成员本身是内存对齐的,它本身所占的大小也满足内存对齐规则。紧跟的其他成员必须在它对齐之后的内存位置,没办法去填充前一个结构体成员的内存空间使其对齐。
空结构体字段占1个字节
struct NullS {};
struct NullChild : public NullS {
int a;
};
struct ON {
NullS a;
};
struct OIN {
int a;
NullS b;
};
struct ONI {
NullS a;
int b;
};
struct ONSI {
NullS a;
short b;
int c;
};
struct ONCCBI {
NullS a;
char b;
char c;
bool d;
int f;
};
struct ONCCBBI {
NullS a;
char b;
char c;
bool d;
bool e;
int f;
};
void align_null_struct() {
std::cout << "sizeof NullS: " << sizeof(NullS) << std::endl;
std::cout << "sizeof NullChild: " << sizeof(NullChild) << std::endl;
std::cout << "sizeof ON: " << sizeof(ON) << std::endl;
std::cout << "sizeof OIN: " << sizeof(OIN) << std::endl;
std::cout << "sizeof ONI: " << sizeof(ONI) << std::endl;
std::cout << "sizeof ONSI: " << sizeof(ONSI) << std::endl;
std::cout << "sizeof ONCCBI: " << sizeof(ONCCBI) << std::endl;
std::cout << "sizeof ONCCBBI: " << sizeof(ONCCBBI) << std::endl;
}
结果
sizeof NullS: 1
sizeof NullChild: 4
sizeof ON: 1
sizeof OIN: 8
sizeof ONI: 8
sizeof ONSI: 8
sizeof ONCCBI: 8
sizeof ONCCBBI: 12
C++语言标准中规定了这样一个原则:“no object shall have the same address in memory as any other variable”,即任何不同的对象不能拥有相同的内存地址。如果空类对象大小为0,那么此类数组中的各个对象的地址将会一致,明显违反了此原则。
C++编译器会在空类或空结构体中增加一个虚设的字节,以确保不同的对象都具有不同的地址。
本次测试得出几个结论:
- 空结构体的大小为1
- 当
struct{}
作为结构体字段时,它也占用1个字节,需要内存对齐。 - 空结构体作为基类时候,不会增加子类的空间占用。
修改对齐值
使用#pragam pack(n),和alignas更改编译对齐方式,但是本文不讨论。