使用工具:
本文输出内存模型的工具为vs2017开发人员命令提示符,使用方法如图所示:
我们使用cl(不是C1,不是C一,是cl,LLLLL)命令cl的语法为:
cl [filename].cpp /d1reportSingleClassLayout[className]
意思是查看[filename].cpp文件中名称以[className]开头的所有类的内存模型
如果文件不在同一目录下,要使用绝对路径。
正文:
含有虚函数的类:
在C++中,虚函数表并不独立存在,而是存在于虚表中
下面就通过例子来解析虚表的结构,以32位操作系统为例:
class A
{
public:
int a_;
virtual void foo1() { cout << "A1" << endl; }
virtual void foo2() { cout << "A2" << endl; }
virtual void foo3() { cout << "A3" << endl; }
};
对于类A,他的内存模型和虚表结构是这样的:
前面的&A_meta代表RTTI信息,0代表虚函数指针到该类顶部的偏移量(众所周知,在非虚继承的情况下,虚函数指针是存在于类的顶部的);
虚继承:
class B :virtual public A
{
public:
int b_;
virtual void foo1() { cout << "B1" << endl; }
virtual void foo4() { cout << "B2" << endl; }
virtual void foo3() { cout << "B3" << endl; }
};
类B虚继承于类A,他的内存模型和虚表结构如下:
我们可以看到,在虚继承的情况下,对于新增加的虚构函数foo4,有一个新的虚函数表指针指向它;对于重写的函数(override),则覆盖到基类的虚函数表中,如果虚继承的子类中没有新的虚函数,那新的虚函数表指针也将不复存在;
而对于虚基类表$vbtable@,第一个位置存放的数据我们可以推测出是类B中,虚基类A的首地址距离类B的最后一个元素(本例中即b_)的偏移量,(8-12=-4),第二个位置存放的是虚基类A的大小;
对于B中存放的虚基类A的虚函数表$vftable@A@,第一个位置变成了-12,表示类B中的虚基类A距离该类的首地址的偏移量(0-12=-12);
普通继承:
class A2
{
public:
int a2_;
virtual void foo1() { cout << "A21" << endl; }
virtual void foo2() { cout << "A22" << endl; }
virtual void foo3() { cout << "A23" << endl; }
};
class C :public A2
{
public:
int c_;
virtual void foo1() { cout << "C1" << endl; }
virtual void foo2() { cout << "C2" << endl; }
virtual void foo3() { cout << "C3" << endl; }
};
类C普通地继承于类A2,类A2中的元素放在类C的低地址,并且只有一个虚函数表指针(在C中就是C的,在A2中就是A2的),同名函数的地址覆盖类A2虚函数表中的函数的地址。
菱形结构多重继承:
class Ori
{
public:
int a;
virtual void foo1() { cout << "Ori" << endl; }
};
class V1 :virtual public Ori
{
public:
int v1;
virtual void foo2() { cout << "V1" << endl; }
};
class V2 :virtual public Ori
{
public:
int v2;
virtual void foo3() { cout << "V2" << endl; }
};
class V3 :public V1, public V2
{
public:
int v3;
virtual void foo4() { cout << "V3" << endl; }
};
对于菱形继承问题,我们可以看到,最开始被虚继承的类Ori被放到最后面,而因为类V3通过普通的多重继承,继承于V1和V2,那自然也不会有新的虚表指针,类V3中的新虚函数foo4,被放到他第一个继承的类V1的虚函数表的最后;
类V2因为在类V3中从虚拟地址12开始,因此偏移量为-12,后面V1和V2的虚基类表和V3的虚函数表可以参考上文,值得一说的是,为什么V1的大小被表示为24,而V2的大小被表示为12,大家可以自己思考一下;
既有普通继承,也有多重继承:
class Base1
{
public:
int base1;
virtual void foo1() {}
virtual void foo2() {}
};
class VBase2
{
public:
int vBase1;
virtual void foo1() {}
virtual void foo2() {}
};
class DD:public Base1,virtual public VBase2
{
public:
int dd;
virtual void foo1() {}
virtual void foo3() {}
};
和预想中的一样,虚继承的类被放在最后面,而子类中重写的虚函数地址覆盖了普通继承的父类的虚函数表中的虚函数地址,而新的虚函数的地址加到了普通继承父类的虚函数表的最后(其实是选择的第一个非虚函数父类);
而在虚继承的父类的虚函数表中,我们可以看到它使用了thunk和goto,让我们使用调用foo1的时候会自动跳转到DD类的foo1函数,这样就保证了多态的正确性;
暂时结束,文中有误的地方,欢迎各位大佬指正!