【CPlusPlusThings笔记】内存对齐(类大小计算)
内存对齐
什么是内存对齐?
假设我们同时声明两个变量:
char a;
short b;
用&(取地址符号)观察变量a,b的地址的话,我们会发现(以16位CPU为例):如果a的地址是0x0000,那么b的地址将会是0x0002或者是0x0004。
那么就出现这样一个问题:0x0001这个地址没有被使用,那它干什么去了? 答案就是它确实没被使用。 因为CPU每次都是从以2字节(16位CPU)或是4字节(32位CPU)的整数倍的内存地址中读进数据的。如果变量b的地址是0x0001的话,那么CPU就需要先从0x0000中读取一个short,取它的高8位放入b的低8位,然后再从0x0002中读取下一个short,取它的低8位放入b的高8位中,这样的话,为了获得b的值,CPU需要进行了两次读操作。
但是如果b的地址为0x0002,那么CPU只需一次读操作就可以获得b的值了。所以编译器为了优化代码,往往会根据变量的大小,将其指定到合适的位置,即称为内存对齐(对变量b做内存对齐,a、b之间的内存被浪费,a并未多占内存)。
结构体(类)内存对齐规则
结构体所占用的内存与其成员在结构体中的声明顺序有关,其成员的内存对齐规则如下(在没有#pragam pack宏的情况下):
-
每个成员分别按自己的对齐字节数和PPB(指定的对齐字节数,32位机默认为4)两个字节数最小的那个对齐,这样可以最小化长度。如在32bit的机器上,int的大小为4,因此int存储的位置都是4的整数倍的位置开始存储。
-
复杂类型(如结构体)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,如结构体数组的时候,可以最小化长度。
-
结构体对齐后的长度必须是成员中最大的对齐参数(PPB)的整数倍,这样在处理数组时可以保证每一项都边界对齐。
-
结构体作为数据成员的对齐规则:在一个struct中包含另一个struct,内部struct应该以它的最大数据成员大小的整数倍开始存储。如 struct A 中包含 struct B, struct B 中包含数据成员 char, int, double,则 struct B 应该以sizeof(double)=8的整数倍为起始地址。
实例演示
struct A
{
char a; //内存位置: [0]
double b; // 内存位置: [8]...[15]
int c; // 内存位置: [16]...[19] ---- 规则1
}; // 内存大小:sizeof(A) = (1+7) + 8 + (4+4) = 24, 补齐[20]...[23] ---- 规则3
struct B
{
int a, // 内存位置: [0]...[3]
A b, // 内存位置: [8]...[31] ---- 规则2
char c, // 内存位置: [32]
}; // 内存大小:sizeof(B) = (4+4) + 24 + (1+7) = 40, 补齐[33]...[39]
注释:(1+7)表示该数据成员大小为1,补齐7位;(4+4)同理。(1+7)的原因是:根据规则2,结构体A的最长的成员为double,所以默认对齐大小为8字节。
类大小计算
规则
首先来个总结,然后下面给出实际例子,实战!(字节即byte)
-
空类的大小为1字节
-
一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间。
-
对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
-
普通继承,派生类继承了所有基类的函数与成员,要按照字节对齐来计算大小。
-
虚函数继承,不管是单继承还是多继承,都是继承了基类的vptr。(32位操作系统4字节,64位操作系统 8字节)!
-
虚继承,继承基类的vptr。
案例
注意:gcc中默认#pragma pack(4),即默认4字节对齐。可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。
原则1
class A{}; // 空类的大小为1字节
原则2
class A
{
public:
char b;
static int c;
static int d;
static int f;
}; // 内存大小:1字节,成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间
class A
{
public:
virtual void fun() {};
static int c;
static int d;
static int f;
}; // 8字节,因为vptr;虚函数本身不占用类对象的存储空间。
class A
{
public:
char b;
virtual void fun() {};
static int c;
static int d;
static int f;
}; // 内存大小:16字节,由于vptr占8个字节,所以根据‘结构体内存对齐规则2,默认对齐大小为8字节,所以对char进行(1+7)。’
静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员,但不影响类的大小。
不管这个类产生了多少个实例,还是派生了多少新的类,静态数据成员只有一个实例。静态数据成员,一旦被声明,就已经存在。
原则3
class A{
virtual void fun();
virtual void fun1();
virtual void fun2();
virtual void fun3();
}; //内存大小:8字节
对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
原则4和5
class A
{
public:
char a;
int b;
}; //内存大小:(1+3)+ 4 = 8
/**
* @brief 此时B按照顺序:
* char a
* int b
* short a
* long b
* 根据字节对齐4+4+8+8=24
* (Note:by nuo):上面是在64位下结果。32位下的实验结果:根据字节对齐4+4+(2+2)
* 或编译器优化
* char a
* short a
* int b
* long b
* 根据字节对齐2+2+4+8=16
* (Note:by nuo):上面是在64位下结果。32位下的实验结果:根据字节对齐(1+1)+2+4+4=12
*/
class B:A
{
public:
short a;
long b;
};
/**
* 把A的成员拆开看,char为1,int为4,所以是(1+3)+4+(1+3)=12
*/
class C
{
A a;
char c;
};
class A1
{
virtual void fun(){}
};
class C1:public A
{
};
-
普通单继承,继承就是基类+派生类自身的大小(注意字节对齐)。
注意:类的数据成员按其声明顺序加入内存,无访问权限无关,只看声明顺序。 -
虚单继承,派生类继承基类vptr
原则6
class A
{
virtual void fun() {}
};
class B
{
virtual void fun2() {}
};
class C : virtual public A, virtual public B
{
public:
virtual void fun3() {}
};
/**
* @brief 8 8 16 派生类虚继承多个虚函数,会继承所有虚函数的vptr
*/
cout<<sizeof(A)<<" "<<sizeof(B)<<" "<<sizeof(C);
类型大小区别
32位编译器与64位编译器中类型大小区别
char | char* | short int | int | unsigned int | float | double | long | unsigned long | long long | |
---|---|---|---|---|---|---|---|---|---|---|
32位 | 1 | 4 | 2 | 4 | 4 | 4 | 8 | 4 | 4 | 8 |
64位 | 1 | 8 | 2 | 4 | 4 | 4 | 8 | 8 | 8 | 8 |
char*(即指针变量,只与地址寻址范围有关): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义