CC++内存对齐以及类的⼤⼩计算详解
尽管内存是以字节为单位,但是⼤部分处理器并不是按字节块来存取内存的。它⼀般会以双字节,四字节,8字节,16字节甚⾄32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度。
现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。
每个特定平台上的编译器都有⾃⼰的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这⼀系数。
有效对其值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较⼩的那个。有效对齐值也叫对齐单位。
内存对其规则:
(1)结构体第⼀个成员的偏移量(offset)为0,以后每个成员相对于结构体⾸地址的 offset 都是该成员⼤⼩与有效对齐值中较⼩那个的整数倍,如有需要编译器会在成员之间加上填充字节。
(2)结构体的总⼤⼩为有效对齐值 的整数倍,如有需要编译器会在最末⼀个成员之后加上填充字节。
参考下⾯的⼏种情况来理解。
//32位系统
#include<stdio.h>
struct
{
int i;
char c1;
char c2;
}x1;
struct{
char c1;
int i;
char c2;
}x2;
struct{
char c1;
char c2;
int i;
}x3;
int main()
{
printf("%d\n",sizeof(x1));  // 输出8
printf("%d\n",sizeof(x2));  // 输出12
printf("%d\n",sizeof(x3));  // 输出8
return 0;
}
上⾯三个结构体的内存布局为:
假定对齐系数为4,因为有效对齐值为 对齐系数和最长数据类型值中较⼩的那个为4,所以结构体中的
成员除了第⼀个偏移量为0,其他成员的偏移量为该成员的整数倍。
对于结构体x1,第⼀个数据成员为int i ,4个字节⼤⼩,char c1 1个字节⼤⼩,偏移为1的整数倍即为4,即接着int 后⾯存放,char c2 1个字节⼤⼩,偏移为5接着c1后⾯放,整体⼤⼩为4+1+1=6,根据规则(2),结构体的⼤⼩为4的整数倍,即为8;
对于结构体x2,第⼀个数据成员为char c1 1个字节⼤⼩,int i 4个字节⼤⼩,偏移为4的整数倍即为4,所以存放在偏移量为4的位置,占4个字节,char c2 1个字节⼤⼩,偏移为1的整数倍即为8,紧接着int i 后⾯存放,整体⼤⼩为 4+4+1=9,根据规则(2),结构体的⼤⼩为4的整数倍,即为12;
对于结构体x3,第⼀个数据成员为char c1 1个字节⼤⼩,char c2 1个字节⼤⼩,偏移为1接着c1后⾯放,int i 4个字节⼤⼩,偏移为4,所以存放在偏移量为4的位置,占4个字节,所以整体⼤⼩为4+4=8,根据规则(2),结构体的⼤⼩为4的整数倍,即为8。
由上⾯的学习,对于内存对齐的计算已经了解,下⾯说⼀下如何计算类的⼤⼩。
C++类涉及空类、静态成员、普通成员函数、静态成员函数、虚成员函数、多继承、虚继承等。
类作为⼀种类型定义是没有⼤⼩可⾔的,这⾥的⼤⼩指的是类的对象所占的⼤⼩,使⽤sizeof对⼀个类型名操作,得到的是具有该类型实体的⼤⼩,计算遵循结构体的对齐原则。
类的⼤⼩与普通数据成员有关,与成员函数和静态成员⽆关。虚函数对类的⼤⼩有影响,因为虚函数表指针带来的影响,同样虚继承也是同理。(静态数据成员之所以不计算在对象⼤⼩内,因为类的静态数据成员被该类所有对象所共享,并不属于哪个对象,定义在内存的全局区);
1.空类的⼤⼩
特别的,空类的⼤⼩为1,C++标准规定,⼀个独⽴对象必须具有⾮零⼤⼩,因为new需要分配不同的内存地址,不能分配内存⼤⼩为0的空间;同时避免除以sizeof(T)时得到除以0的错误,因此使⽤1个字节来区分空类。
空类的继承:当派⽣类继承空类后,如果派⽣类有⾃⼰的数据成员,空基类的⼀个字节⼤⼩并不会加到派⽣类中,⽐如下⾯这种情况,sizeof(A)结果为4。
class Empty{};
struct A:public Empty{ int a;};
//sizeof(A)为4
⼀个类包含⼀个空类对象数据成员,空类的1字节会被计算进去,⽐如下⾯这种情况,根据对齐原则,则sizeof(B)结果为8。
class Empty{};
class B{
int x;sizeof 指针
Empty e;
};  //sizeof(B)为8
2.含有虚成员函数的类的⼤⼩
虚函数是通过⼀张虚函数表来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前⾯的位置(这是为了保证正确取到虚函数的偏移量)。
每当创建⼀个包含有虚函数的类或从包含有虚函数的类派⽣⼀个类时,编译器就会为这个类创建⼀个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作⽤就是保存⾃⼰类中所有虚函数的地址,可以把VTABLE形象地看成⼀个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类中,编译器秘密地置⼊⼀指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。 当构造该派⽣类对象时,其成员VPTR被初始化指向该派⽣类的VTABLE。所以
可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;⽽VPTR则是每个类对象都有独⽴⼀份的,且在该类对象被构造时被初始化。
因此含有虚成员函数的类实例化对象中包含了指向虚函数表的指针,指针的⼤⼩为4(32位系统),因此计算⼤⼩时,需要算上虚表指针的⼤⼩且在对象实例中最前⾯,如下情况。
class Base{
public:
int a;
virtual void f(){ cout<<"Base::f"<<endl; }
virtual void g(){ cout<<"Base::f"<<endl; }
};  //sizeof(Base)为8
3.基类含有虚函数的继承
1. 在派⽣类中不对基类的虚函数进⾏覆盖,同时派⽣类中还有⾃⼰的虚函数,如下派⽣类。虚函数按
照声明顺序放于表中,基类的虚函
数在派⽣类的虚函数的前⾯,此时基类和派⽣类的sizeof都是⼀个指针⼤⼩+数据成员⼤⼩。即只有⼀个虚表指针。
class Son:public Base{
public:
virtual void f1(){ cout<<"Son::f1"<<endl; }
virtual void g1(){ cout<<"Son::g1"<<endl; }
};
2. 在派⽣类中对基类的虚函数进⾏覆盖,派⽣类的⼤⼩仍然是基类和派⽣类的数据成员+⼀个虚表指针的⼤⼩。
3. 多重继承:⽆论是否对虚函数进⾏覆盖,每个基类都需要⼀个指针来指向其虚函数表,派⽣类的虚函数存放在第⼀个基类的虚函数表
中。因此派⽣类的⼤⼩为继承的基类个数的指针加上他的所有数据成员⼤⼩,⽐如下⾯情况。
class A
{
};
class B
{
char ch;
virtual void f0()  {  }
};
class C
{
char ch1;
char ch2;
virtual void f()  {  }
virtual void f1() {  }
};
class D: public B, public C
{
int d;
virtual void f0()  {  }
virtual void f1()  {  }
virtual void f2()  {  }
};
//sizeof(A)为1
//sizeof(B)为8
//sizeof(C)为8
//sizeof(D)为20
对于D类,继承与B和C,有两个虚表指针分别指向B和C的虚函数表,⾸先是指向B虚函数表的指针,然后类B中的数据成员,再然后是指向类C的虚函数表指针,然后类C中的数据成员,最后是类D中的数据成员d。
4. 虚继承的情况 :虚继承时,不仅要计算指向基类的虚函数表指针,如果⾃⾝也有虚函数,则会有单独的虚函数表,即也有⼀个指向⾃
⾝类的虚函数表的指针。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。