C++基础——对象的内存布局
对齐和包裹
事实上每种数据类型有其自然的对齐方式,使用内存对齐的原因如下:
- 现在计算机内存空间都是按照byte字节划分的,理论上讲对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址*问,这就需要各种数据类型按照一定的规则在空间上排列,而不是一个接一个的排放,这就是内存对齐。
- cpu对内存的读取不是连续的而是分块读取的,块的大小只能是2i个字节数,从cpu的读取性能和效率来考虑,若读取的数据未对齐,则需要两次总线周期来访问内存,因而效率会大打折扣
- 另外某些固定的硬件平台只能从规定的相对地址处读取特定类型的数据,否则会产生硬件异常。
- 如果不按照适合平台要求对数据存放进行对齐,会存在效率上的损失。比如有些平台每次读都是从偶地址开始,如果一个int型(32位系统)存放在偶地址开始的地方,那么一个周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要两个读周期,并对两次读出结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
上图说明的是cpu如何与内存进行数据交换的模型,左边是cpu,右边是内存空间,内存上边的0~3是内存地址。这张图以32位cpu作为代表;32位cpu是以双字为单位进行数据传输的,正因为这个原因,如果我们的数据只有8位或16位,cpu是不是就会以我们数据的位数进行传输呢,答案是否定的,这样会使的cpu硬件变得复杂,所以32位cpu传输数据无论是8位或是16位都是以双字进行传输的。
上图描述了对齐和非对齐的读取32位整数的区别:当程序从0x6a341174读取32位整数时,内存控制器可以愉快的载入数据,因为该地址是4字节对齐的;但是若是从0x6a341173载入32位的整数,内存控制期就需要读入两个4字节块(一块位于0x6a341170,另一块位于0x6a341174),然后还需要通过掩码和移位操作取得32位整数的两部分,再用逻辑OR操作把两部分合并,最后把结果写入目标寄存器。
内存对齐规则
在不用#pagrama pack()包裹的情况下,结构体或联合体按照编译器默认的对齐方式有以下三个对其原则:
- 数据成员对齐原则:结构(struct或union)的数据成员,第一个数据成员存放在offset为0的地方,以后每个数据成员存储的起始位置都要从该成员占用内存大小的整数倍开始
- 结构体作为成员的原则:如果一个结构中有某些结构体成员,则结构的成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始)
- 结构(或联合)的整体对齐原则:在数据成员各自对齐后,结构(或联合)本身也要进行对齐,即以结构体内部占用内存空间最大的数据类型进行对齐。(等同于sizeof该结构体的结果必须是其内部最大成员占用内存的整数倍)
#include <iostream>
using namespace std;
struct MyStruct
{
int i;
char a;
float f;
};
struct MyStruct1
{
int i;
char a;
double f;
};
struct MyStruct2
{
int a;
MyStruct1 stuct;
};
int main()
{
MyStruct myStruct;
cout << "&i = " << &myStruct.i << endl;
cout << "&a = " << (void*)&myStruct.a << endl;
cout << "&f = " << &myStruct.f << endl;
MyStruct1 myStruct1;
cout << "--------------------------------------" << endl;
cout << "sizeof(double): " << sizeof(double) << endl;
cout << "&i = " << &myStruct1.i << endl;
cout << "&a = " << (void*)&myStruct1.a << endl;
cout << "&f = " << &myStruct1.f << endl;
cout << "--------------------------------------" << endl;
MyStruct2 s;
cout << "&a = " << &s.a << endl;
cout << "&stuct = " << &s.stuct << endl;
return 0;
}
以下为测试结果输出,注意MyStruct2的对象的成员地址,s.stuct地址是从8的整数倍开始:
#pragma pack()自定义数据对齐规则
#pragma pack(n):每个特定平台上的编译器都有自己默认的对齐系数,程序员可以通过预编译指令#pragma pack(n),n=1,2,4,8,16来改变这一系数
- 数据成员对齐规则:结构或联合的数据成员,第一个数据成员在offset为0的地方,以后每个数据成员的对齐方式都按照#pragma pack指定的数值和这个数据成员自身占用内存中比较小的那个进行。
- 结构或联合整体对其原则:在数据成员完成自身对齐后,结构或联合本身也要进行对齐,对齐按照#pragma pack(n)指定的数值和结构或联合最大数据成员占用内存中比较小的那个进行。
#include <iostream>
using namespace std;
struct node
{
char e;
int f;
short int a;
char b;
};
void Memory_Layout_Pragma_Test()
{
node n;
cout << "sizeof(node): " << sizeof(node) << endl;
cout << "&e: " << (void*)&n.e << endl;
cout << "&f: " << (void*)&n.f << endl;
cout << "&a: " << &n.a << endl;
cout << "&b: " << (void*)&n.b << endl;
}
int main()
{
Memory_Layout_Pragma_Test();
return 0;
}
下面修改下测试程序,添加#pragma pack(2),对比下输出结果:
#include <iostream>
using namespace std;
#pragma pack(2)
struct node
{
char e;
int f;
short int a;
char b;
};
void Memory_Layout_Pragma_Test()
{
node n;
cout << "sizeof(node): " << sizeof(node) << endl;
cout << "&e: " << (void*)&n.e << endl;
cout << "&f: " << (void*)&n.f << endl;
cout << "&a: " << &n.a << endl;
cout << "&b: " << (void*)&n.b << endl;
}
int main()
{
Memory_Layout_Pragma_Test();
return 0;
}
当有些时候想用4字节对齐,有些时候又想用1字节或8字节对齐时,便用到了push和pop两个预编译指令来控制对齐方式:
- #pragma pack(push):压栈,编译器编译到此处时将保存对齐状态(即push指令之前的对齐状态)
- #pragma pack(pop):出栈,编译器编译到此处将恢复push指令前保存的对齐状态,在使用#pragma pack(pop)之前需要使用#pragma pack(push)指令
- #pragma pack():能够取消自定义的对齐方式,恢复为默认的对齐方式
类对象的内存布局
c++类对象的内存布局,涉及类的成员变量也就是上述讨论的数据对齐,另外还涉及到继承、虚函数、虚继承的问题,下面先看下包含继承和虚函数类的内存布局:
class Base {
public:
void func() {}
private:
int a;
};
class Base1 {
public:
virtual void func() {}
private:
int a;
};
class Derive : public Base {};
class Derive1 : public Base1 {};
上述测试代码的内存布局如下(显示的内容很详细,如需查看内存布局,请参看C++基础——虚继承以及实现原理):
关系虚函数的详细内容:C++基础——虚指针(vptr)与虚基表(vtable)
关系虚继承的内存布局详细内容:C++基础——虚继承以及实现原理
以上参考:
- https://www.cnblogs.com/southcyy/p/10175163.html
- 游戏引擎架构【美】Jason Gregory 著
本文地址:https://blog.csdn.net/qq_25065595/article/details/107443590