尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的。它一般会以双字节,四字节,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; 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.基类含有虚函数的继承
- 在派生类中不对基类的虚函数进行覆盖,同时派生类中还有自己的虚函数,如下派生类。虚函数按照声明顺序放于表中,基类的虚函数在派生类的虚函数的前面,此时基类和派生类的sizeof都是一个指针大小+数据成员大小。即只有一个虚表指针。
class Son:public Base{ public: virtual void f1(){ cout<<"Son::f1"<<endl; } virtual void g1(){ cout<<"Son::g1"<<endl; } };
-
在派生类中对基类的虚函数进行覆盖,派生类的大小仍然是基类和派生类的数据成员+一个虚表指针的大小。
-
多重继承:无论是否对虚函数进行覆盖,每个基类都需要一个指针来指向其虚函数表,派生类的虚函数存放在第一个基类的虚函数表中。因此派生类的大小为继承的基类个数的指针加上他的所有数据成员大小,比如下面情况。
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。
-
虚继承的情况 :虚继承时,不仅要计算指向基类的虚函数表指针,如果自身也有虚函数,则会有单独的虚函数表,即也有一个指向自身类的虚函数表的指针。