单继承的情况下
若类有虚函数,则在构造函数的时候编译器会自动为类的实例(对象)在其内存的首部(0地址偏移处)增添一个虚函数表指针vfptr,指向该类的虚函数表。虚函数表中会存放该类所有的虚函数地址,普通函数则不会被放入其中。如果是子类重写了父类的虚函数,那么在建立虚函数表的时候被重写的虚函数的地址被替换成了子类的虚函数。
而使用父类指针BaseClass* base指向一个子类对象DerivedClass时,当调用虚函数virtualFunc的时候,其实际执行过程是base->vfptr->virtualFunc , 这样就实现了父类指针调用实际子类的成员函数。
多继承的情况下
普通的多继承
class Base
{
int a;
int b;
public:
void virtual VirtualFunction();
};
class DerivedClass1: public Base
{
int c;
public:
void virtual VirtualFunction();
};
class DerivedClass2 : public Base
{
int d;
public:
void virtual VirtualFunction();
};
class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
{
int e;
public:
void virtual VirtualFunction();
};
DerivedDerivedClass的内存分布中,会有两个vfptr分别指向两个虚函数表(DerivedClass1,DerivedClass2)
同时会存在两份Base类的成员a,b:
虚继承
若改动如下:
class DerivedClass1: virtual public Base
...
class DerivedClass2 : virtual public Base
...
则DerivedClass1和DerivedClass2的内存分布就已经有变化了。
指向virtual base Base的vfptr以及虚基类Base的成员变量被放在了内存的最后部分:
即额外增加了一个虚函数表指针vbptr指向派生类的虚函数表。
并且最终DerivedDerivedClass的内存分布如下:
、
可见,虚继承是通过增加额外的虚函数表指针来达到保证基类的数据只有一份的特性。
虚函数表什么时候初始化?在内存哪一块?
虚函数表指针vptr是每个对象实例都有一份的,其指向的虚函数表是属于类的,每个类都有自己单独的唯一一份虚函数表,放置在只读数据段.rodata。虚函数表是由一个个的虚函数指针组成的。.rodata只读数据段里面存放的还有由const修饰的只读数据。
虚函数表中的偏移量有何作用?
可以看到虚函数表中,DerivedClass1中的20表明它的虚指针{vbptr}离虚基表指针{vfptr}的距离。同理DerivedClass2的12。最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}在内存中的偏移。
这些偏移是为了让类指针可以找到正确的虚函数表。
例如:
DerivedDerivedClass ins;
DerivedClass2 *d2 = ins;
ins.virtualFunction();
d2->virtualFunction();
ins在调用virtualFunction()的时候,先找到其正确的虚函数表指针,对于DerivedDerivedClass类来说,其正确的虚函数表指针是位于该类内存地址偏移20处的vfptr,那么调用虚函数的时候编译器如何知道要偏移20呢?
查看汇编指令发现,调用虚函数的时候指针或引用都是直接取内存首地址的,这就要求该引用和指针一开始就指向正确的虚函数表指针。即在ins构造的时候,ins引用就指向了内存偏差20处的vfptr。故在声明引用的时候,编译器就会去查引用、指针类的虚函数表,并用虚函数表中事先存的偏移量来初始化引用、指针。
参考:
探索C++虚函数在g++中的实现
https://www.cnblogs.com/senior-engineer/p/7832915.html
C++类内存分布
http://www.cnblogs.com/jerry19880126/p/3616999.html