此处举例在VS2010中编译:
分析继承与多态虚函数在内存中的分配布局和分析理解过程,直接以多继承举例:
首先说明虚表的概念:虚表即一个类中存放其虚函数地址的表,这个表的地址则存放在此类内存分布中,即一个_vptr的指针;
虚表基本构建:一个类有多个虚函数时,其按照虚函数在类中出现的先后次序填写虚表,最后在虚表后加上0x00000000.
虚表性质:虚表在一个类对象调用构造函数前已经形成,构造函数只是将虚表地址赋给内存的前四个字节,即为指针_vptr.
1.继承中没有虚函数,举例:
class Base1 { public: void Func1() { cout<<"Base1"<<endl; } public: int _a; }; class Base2 { public: void Func2() { cout<<"Base2"<<endl; } public: int _b; }; class Derived:public Base1,public Base2 { public: void FuncD() { cout<<"Derived"<<endl; } public: int _d; }; void Func() { Derived d; d._a=1; d._b=2; d._d=3; int size=sizeof(d); Base1* p1=&d; Base2* p2=&d; Derived* p3=&d; }Derived公有继承Base1,Base2,查看内存:
即p1、p3与&d对象地址相同,p2偏移四个字节,d对象总大小为12,则内存分布为:
2.只将以上Base1与Base2函数前加上virtual,变为虚函数,派生类Derived内容不变,查看内存情况:
Base1:
virtual void Func1() { cout<<"Base1"<<endl; }Base2:
virtual void Func2() { cout<<"Base2"<<endl; }
Derived d; Base1 b1; Base2 b2;
添加两个父类对象,查看内存情况:
b1
b2:
所以当加上virtual关键字后,基类对象各自多增加四个字节,其则为虚表地址_vfptr。再查看继承后的内存分布:
&d地址存储内容如下:
取Base1的_vptr得出虚函数表:
取Base2的_vptr得出虚函数表:
即各自为基类虚函数,并且p1、p3与&d对象地址相同,p2偏移8个字节,d对象总大小为20,所以可以得出内存分布为:
3.在派生类Derived中重写基类中的虚函数Func1()、Func2(),如:
class Derived:public Base1,public Base2 { public: virtual void Func1() { cout<<"Derived1"<<endl; } virtual void Func2() { cout<<"Derived2"<<endl; } public: int _d; };此时在查看内存分布:
虚表为:
Base1
Base2
由上可知此时与第二种情况的内存格局的分布没有任何变化,可是它们的区别又是什么呢?查看以下调用:
若去掉刚才基类和派生类关键字virtual或者派生类没有重写基类函数,用父类指针或引用进行调用此函数,运行结果如下:
class Base1 { public: virtual void Func1() { cout<<"Base1"<<endl; } public: int _a; }; class Base2 { public: virtual void Func2() { cout<<"Base2"<<endl; } public: int _b; }; class Derived:public Base1,public Base2 { public: /*virtual void Func1() { cout<<"Derived1"<<endl; } virtual void Func2() { cout<<"Derived2"<<endl; }*/ public: int _d; }; void Fun1(Base1* p1) { p1->Func1(); } void Fun2(Base2* p2) { p2->Func2(); } void Fun3(Derived* p3) { p3->Func1(); p3->Func2(); } void Func() { Derived d; Fun1(&d); Fun2(&d); Fun3(&d); Base1 b1; Base2 b2; d._a=1; d._b=2; d._d=3; int size=sizeof(d); Base1* p1=&d; Base2* p2=&d; Derived* p3=&d; }
为派生类重写的运行结果。
这时的原因即为C++的多态的另一类运行时多态,动态连编,在运行时确定指针所指的真正对象类型,调用其函数。
这一过程实现的机制则为:第2种情况和第3对象d内存分布格局相同,调用函数运行结果不同,原因在于创建虚表时函数地址发生改变,此时创建虚表的过程为:Derived先后继承Base1,Base2,所以(1)先创建两个基类虚表,按虚函数在基类中出现的先后顺序填写虚表;(2)基类完成后创建派生类虚表,过程先编译器了解基类虚表格式,再了解派生类对基类哪些虚函数进行了重写;(3)有重写的,则将虚表相同位置的函数地址修改为派生类重写的新的虚函数地址;
(4)若派生类有自己的新的虚函数,则将它加在派生类先继承的基类的虚表后,证明如下:
class Derived:public Base1,public Base2 { public: //派生类重写两个基类虚函数 virtual void Func1() { cout<<"Derived1"<<endl; } virtual void Func2() { cout<<"Derived2"<<endl; } //派生类自己虚函数 virtual void Func3() { cout<<"Derived3"<<endl; } public: int _d; };查看内存情况
:
内存格局未变,查看虚函数表:
Base1:
Base2
可知Base1虚表增加了一个函数地址,其就为派生类自己的虚函数。
(5)最后在虚表之后加上0X00000000.
4.若派生类在变为虚拟公有继承基类,如:
class Derived:virtual public Base1,virtual public Base2查看内存情况:
此时&d与p3相同,p1偏移12字节,p2偏移20个字节,总大小为28,在查看各地址:
由上内存监视知:即为派生类Derived虚表;
即为Base1虚表;
即为Base2虚表;
可知第二个内存空间存放偏移表地址,偏移表里存放从当前位置开始与自己、两基类的开始位置偏移字节。
内存分布格局如下:
以上即为以多继承为例剖析多态虚函数的内存布局与分析过程。