大家都知道虚函数是C++的精髓,虚函数和继承配合则展现出了多态这一强大的特性,为了剖析此类机制下的对象内存布局问题,下面使用多个实例代码展现其中的内存模型分布。(读《深度探索C++对象模型》有感)。说明:下面所有的代码均在win10和VS2019开发环境下跑通。
1、单继承下基类只有一个虚函数
class Base
{
public:
virtual void funcBase() {}
public:
int dataBase;
};
class Derived : public Base
{
public:
int dataDerived;
};
int main(int argc, char* argv[])
{
Derived d;
std::cout << sizeof(d) << std::endl;
return 0;
}
下面使用VS2019自带的x86 Native Tools Command Prompt工具打印出对象的内存分布图。方法如下,首先进入x86 Native Tools Command Prompt工具环境中,使用cd命令直到进入到存放cpp代码文件的目录,而后使用命令
cl main.cpp /d1 reportSingleClassLayoutDerived
(注意字母l和数字1)。一般reportSingleClassLayout后面跟随的就是你想打印出对象所属的类名。这里是Derived,因此最后是这样的命令格式reportSingleClassLayoutDerived,打印出的内存布局如下:
下面就此说明下,第一行打印出这个类的内存大小为12字节。
下面是类Derived的内存布局了,由于Derived继承自Base类,并且Base类中有一个虚函数,因此出现了一个虚函数表指针vfptr,左边的0表示的是偏移量,0表示开始的基准位置。虚函数表指针之后是Base类中的数据成员dataBase,因为虚函数表指针是一个指针,在32位机器中占据4个字节,因此dataBase的起始位置是4,最后存放的是Derived类中的数据成员dataDerived,dataBase是int型,占4个字节,所以dataDerived的起始位置偏移量是8。dataDerived也是int型,也占4个字节,因此总共类的字节大小是12字节。
2、单继承,基类和子类中均含有虚函数和数据成员
class Base
{
public:
virtual void func() {}
public:
int dataBase;
};
class Derived : public Base
{
public:
virtual void func1() {}
public:
int dataDerived;
};
int main(int argc, char* argv[])
{
Derived d;
std::cout << sizeof(d) << std::endl;
return 0;
}
使用命令
cl main.cpp /d1 reportSingleClassLayoutDerived
咦?类的大小怎么还是12个字节。且看答案:和情景1中一样,类Derived继承自类Base,内存首先存放的是虚函数表指针,从vftable中可以看出,虚函数表指针中存放了两个虚函数地址,一个是基类Base中的虚函数,紧接着才是Derived类中的虚函数。尽管这里多一个虚函数,但是并不影响类内存大小。
3、多重继承,基类和子类中均含有虚函数和数据成员
class A
{
public:
virtual void func1() {}
public:
int data1;
};
class B : public A
{
public:
virtual void func2() {}
public:
int data2;
};
class C : public B
{
public:
virtual void func3() {}
public:
int data3;
};
int main(int argc, char* argv[])
{
C c;
std::cout << sizeof(c) << std::endl;
return 0;
}
多重普通继承下,最先调用的是祖先类构造函数,而后是父类的构造函数,最后才是子类自己的构造函数。这里需要注意的是在虚函数表中,首先存放A中虚函数地址,而后是B中虚函数,最后才是C中虚函数。下面是C中虚函数出现覆盖基类的情况
class A
{
public:
virtual void func1() {}
public:
int data1;
};
class B : public A
{
public:
virtual void func2() {}
public:
int data2;
};
class C : public B
{
public:
virtual void func1() {}//覆盖A中的func1函数
public:
int data3;
};
int main(int argc, char* argv[])
{
C c;
std::cout << sizeof(c) << std::endl;
return 0;
}
和上面的内存布局图相比,确实最后子类的虚函数地址覆盖了基类A中同名虚函数的地址
4、多继承,基类和子类中均含有虚函数和数据成员
class A
{
public:
virtual void funcA1() {}
virtual void funcA2() {}
public:
int data1;
};
class B
{
public:
virtual void funcB1() {}
virtual void funcB2() {}
public:
int data2;
};
class C : public A, public B
{
public:
virtual void funcC1() {}
virtual void funcC2() {}
public:
int data3;
};
类C的内存布局图:
在多继承下,继承的两个类中均含有虚函数,因此有两个虚函数表存在,就有两个虚函数表指针。这里需要注意的是子类C中的虚函数存放的是在A中最后一个虚函数后面依次存放。
5、单虚继承,虚基类和派生类均含有虚函数和数据成员
class A
{
public:
virtual void funcA1() {}
public:
int data1;
};
class C : virtual public A
{
public:
virtual void funcC2() {}
public:
int data2;
};
类C的内存布局图:
在虚拟继承下,会多出来一个虚基类表,因此就有虚基类表指针的存在。这里派生类中含有虚函数,在虚继承下,内存首先为派生类进行分配空间,因此先存放的是属于C中的虚函数表指针,里面存放的是C中的虚函数的地址。而后是虚基类表指针,指向的是一个虚基类表,虚基类表中主要存放的是两类内容,第一类是派生类内存首地址相对于虚基类表指针地址的偏移量,第二类内容是虚基类的内存首地址相对于虚基类表指针地址的偏移量。看vbtable中的上下两项,第一项说明的是属于C中虚表指针在先,而后才是虚基类表指针,-4表示向前4个字节;而第二项说明的是虚基类A的首地址在虚基类表指针地址之后8个字节的位置。
如果派生类中没有虚函数的情况:
class A
{
public:
virtual void funcA1() {}
public:
int data1;
};
class C : virtual public A
{
public:
int data2;
};
派生类中没有虚函数,也就没有了虚函数表指针,内存起始位置存放的就是虚基类表指针指针,因此虚基类表中的第一项地址偏移量是0。第二项还是向后8个字节的位置。
6、多虚拟继承下的对象内存布局情况
class A
{
public:
virtual void funcA1() {}
public:
int data1;
};
class B
{
public:
virtual void funcB1() {}
public:
int data2;
};
class D
{
public:
virtual void funcD1() {}
public:
int data4;
};
class C : virtual public A, virtual public B, virtual public D
{
public:
virtual void funcC1() {}
public:
int data3;
};
虚基类和派生类中均含有虚函数,并且均有数据成员
派生类C的对象内存布局是:
第一个存放派生类中的虚函数表指针
第二个存放的是虚基类表指针。虚基类表指针指向的是一个虚基类表。虚基类表中第一项是派生类地址相对于虚基类表指针地址的偏移,这里是-4,
因为C的虚表指针在前,虚基类表指针在后,一个指针的字节大小是4.虚基类表中第二项开始依次存放的是虚基类A地址相对于虚基类表指针地址
的偏移量,这里是8;之后是虚基类B地址相对于虚基类表指针地址的偏移量,这里是16;再之后是虚基类D相对于虚基类表指针地址的偏移量,
这里是24.
第三个存放的是派生类C中自己的数据成员
第四个存放的是虚基类A的虚表指针
第五个存放的是虚基类A中的数据成员
第六个存放的是虚基类B的虚表指针
第七个存放的是虚基类B中的数据成员
第八个存放的是虚基类D的虚表指针
第九个存放的是虚基类D中的数据成员
总共sizeof(C)是36个字节,按照4字节对齐的方式
7、多继承中基于虚拟继承又有普通继承情况下
class A
{
public:
virtual void funcA1() {}
public:
int data1;
};
class B
{
public:
virtual void funcB1() {}
public:
int data2;
};
class D
{
public:
virtual void funcD1() {}
public:
int data4;
};
class C : virtual public A, public B, public D
{
public:
virtual void funcC1() {}
public:
int data3;
};
谁有虚函数,谁就先存储。比方说某一个基类中有函数,另外的没有,那么先存储这个基类的虚表指针,之后再是该基类的数据成员。而后才是其他普通继承的基类的数据成员。普通继承的基类内存分布完毕之后,是虚基类表指针,之后是派生类自己的数据成员。最后才是虚基类中的虚表指针和虚基类A中的数据成员。
8、菱形继承下的对象内存布局
class A
{
public:
virtual void funcA1() {}
public:
int data1;
};
class B : virtual public A
{
public:
virtual void funcB1() {}
public:
int data2;
};
class C : virtual public A
{
public:
virtual void funcC1() {}
public:
int data3;
};
class D : public B, public C
{
public:
virtual void funcD1() {}
public:
int data4;
};
由于类D多继承自类B和类C,先看类B和类C中哪个有虚函数,一看均有,那么首先存放的是类B的虚函数表
第一个内存空间存放的是类B的虚函数表指针,由于派生类D中也有虚函数,因此也存放在这个虚函数表中
第二个内存空间存放的是属于类B中的虚基类表指针,因为类B虚继承自类A
第三个内存空间存放的是类B的数据成员
第四个内存空间存放的是类C的虚函数表
第五个内存空间存放的是类C的虚基类表指针
第六个内存空间存放的是类C的数据成员
第七个内存空间存放的是派生类D的数据成员
第八个内存空间存放的是虚基类A的虚函数表指针
第九个内存空间存放的是虚基类A的数据成员
9、菱形继承下带有虚拟继承和普通继承的多继承
class A
{
public:
virtual void funcA1(){}
public:
int data1;
};
class B : virtual public A
{
public:
virtual void funcB1() {}
public:
int data2;
};
class C : virtual public A
{
public:
virtual void funcC1() {}
public:
int data3;
};
class D : virtual public B, public C
{
public:
virtual void funcD1() {}
public:
int data4;
};
第一个内存空间存放的是类C的虚函数表指针,由于派生类D中也有虚函数,因此也存放在这个虚函数表中
第二个内存空间存放的是属于类C中的虚基类表指针,因为类C虚继承自类A
第三个内存空间存放的是类C的数据成员
第四个内存空间存放派生类D的数据成员
第五个内存空间存放的是虚基类A的虚函数表指针
第六个内存空间存放的是虚基类A的数据成员
第七个内存空间存放的是类B的虚函数表
第八个内存空间存放的是类B的虚基类表指针
第九个内存空间存放的是类B的数据成员
10、菱形继承下纯带有虚拟继承的多继承
class A
{
public:
virtual void funcA1() {}
public:
int data1;
};
class B : virtual public A
{
public:
virtual void funcB1() {}
public:
int data2;
};
class C : virtual public A
{
public:
virtual void funcC1() {}
public:
int data3;
};
class D : virtual public B, virtual public C
{
public:
virtual void funcD1() {}
public:
int data4;
};
第1: D虚表指针
第2:D虚基类表指针
第3:D数据成员data4
第4:A虚表指针
第5:A数据成员data1
第6:B虚表指针
第7:B虚基类表指针
第8:B数据成员data2
第9:C虚表指针
第10:C虚基类表指针
第11:C数据成员data3