C++中虚继承产生的虚基类指针和虚基类表,虚函数产生的虚函数指针和虚函数表

本博客主要通过查看类的内容的变化,深入探讨有关虚指针和虚表的问题。

一、虚继承产生的虚基类表指针和虚基类表

如下代码:写一个棱形继承,父类Base,子类Son1和Son2虚继承Base,又来一个类Grandson继承Son1和Son2。
代码:

class Base
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son1 :virtual public Base 
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son2 :virtual public Base 
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Grandson :public Son1,public Son2
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

首先对Base的内容是肯定的,三个整形变量,没有其它东西了,如下图:
在这里插入图片描述
再来看看类Son1和Son2的东西,由于Son1和Son2的类本身内容相同,且都是虚继承Base类,所以Son1和Son2类的内容相同。如下两个图:
在这里插入图片描述
在这里插入图片描述
上两个图是Son1类和Son2类的内容,这两个类创建的对象使用从Base类继承的内容就是委婉地通过vbptr到vbtale中找到继承的Base类内容的首地址访问的。
虚基类表中的16是相对于Son类vbptr地址的偏移量,即继承的Base类的内容在子类中存放的首地址相对于Son的vbptr的地址的偏移量。Son1和Son2都继承父类Base,本来类大小为sizeof(Son)+sizeof(Base)=12+12=24的,但是由于是虚继承,Son类内会多一个虚指针,所以类大小多了4字节,变成了28字节。

再来看看Grandson类的内容:
在这里插入图片描述
从Son1继承的虚基类表里面的44是指Base类的内容到Son1的vbptr的地址偏移量为44。同理,从Son2继承的虚基类表里面的28是指Base类的内容到Son1的vbptr地址偏移量为28。这里可以看出,本来Son1和Son2类里面都有从Base继承的内容,但是由于Son1和Son2都是虚继承,Grandson继承Son1和Son2的时候,Base类的内容只有一份,避免了继承两份Base内容(Son1含一份,Son2含一份)而造成的资源浪费。不管Grandson是通过Son1还是通过Son2访问Base的内容,其实Son1和Son2都是通过自己的vbptr访问同一份Base的内容。
那么,设想一下,如果Grandson也是以虚继承的方式继承Son1和Son2的呢?即把Grandson的继承代码改成以下方式:

class Grandson :virtual public Son1,virtual public Son2
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

Son1和Son2以虚继承的方式继承Base造成的结果是Son1和Son2都多了一个虚基类指针指向虚基类表,虚基类表中记录着从Base继承过来的内容的首地址对Son1(Son2)类的vbptr的地址的偏移量。类比一下,不难想出,Grandson如果也以虚继承的方式继承Son1和Son2的话,那么Grandson本身肯定也会多一个虚基类指针,指向一个虚基类表,这个虚基类表中存放着从Son1继承过来的内容的首地址相对于Grandson类的vbprtr地址的偏移量,也存放着从Son2继承过来的内容的首地址相对于Grandson类的首地址的偏移量。除此之外还存放着一个东西,Base的内容的首地址相对于Grandson类的vbptr的地址的偏移量。所以Grandson的虚基类指针指向虚基类表,虚基类表中存放着三个地址偏移量:
在这里插入图片描述
如上图(Son1和Son2的虚基类指针和虚基类表在上面分析过了,这里主要分析Grandson的虚基类指针和虚基类表),虚基类表中的16、28、44分别对应着包含的Base、Son1、Son2的内容的首地址相对于Grandson的vbptr地址的偏移量。

二、虚函数产生的虚函数指针和虚函数表

注: 在这里不讨论动态多态和静态多态,只讨论分析虚指针和虚表的问题。

首先,vbptr(virtual base pointer)虚基类指针和vfptr(virtual function pointer)虚函数指针是两个不同的东西,是可以共存的,上面已经说明vbptr是对于虚继承而言的产物,vfptr则是对于虚函数的产物。以下代码说明vbptr和vfptr可以共存:

class Base
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son1 :virtual public Base //虚继承(产生虚基类指针vbptr)
{
	virtual void fun() {}//虚函数(产生虚函数指针vfptr)
public:
	int a;
protected:
	int b;
private:
	int c;
};

看一下Son1的结构:
在这里插入图片描述
可以看出vbptr和vfptr是两个不同的指针,而且可以共存。
vbptr指向的是虚基类表,虚基类表中放着Base内容的首地址相对于Son1vbptr指针的地址的偏移量。
vfbptr指向的是虚函数表,虚函数表中放着Son1的虚函数的地址。
由于虚基类指针已经分析过,下面只针对虚函数指针展开分析。如下代码,父类函数虚函数,子类继承父类。

class Base
{
public:
	virtual void fun1() {}
	virtual void fun2() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son1 :public Base 
{
public:
	int a;
protected:
	int b;
private:
	int c;
};

看一下Base类的结构:
在这里插入图片描述
可以看出,由于Base类内函数虚函数,所以Base类会多一个虚函数指针,虚函数指针指向虚函数表,虚函数表中存放着虚函数的地址。
再来看看Son1类里面的东西:
在这里插入图片描述
可以看见Base的虚函数指针被Son1继承下来了,虚函数指针指向一个虚函数表,虚函数表中放着的是Base的两个虚函数的地址。那么如果Son1对Base的虚函数进行重写呢?比如对fun1函数进行重写:

class Base
{
public:
	virtual void fun1() {}
	virtual void fun2() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son1 :public Base 
{
public:
	void fun1() {}//对Base的虚函数fun1函数进行重写
public:
	int a;
protected:
	int b;
private:
	int c;
};

在这里插入图片描述
可以发现,虚函数表中本来存放Base的虚函数fun1()的地址,Son1对Base的fun1函数进行重写后,虚函数表中原本存放Base的虚函数fun1的地址,现在存放了被Son1重写后的fun1函数的地址。这也就是我们说的当子类重写父类的某一个虚函数时,父类的这个虚函数会被重写后的函数覆盖。
再想一下,如果Son1本身也有虚函数呢?和vbptr类比一下,会觉得如果Son1本身有虚函数的话,那么Son1本身也会有一个虚函数指针,指向一个虚函数表,虚函数表中放着Son1的虚函数的地址。但是事实并非如此!
如下所示:

class Base
{
public:
	virtual void fun1() {}//对Base的虚函数fun1函数进行重写
	virtual void fun2() {}//Son1自己的虚函数
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son1 :public Base 
{
public:
	void fun1() {}
	virtual void fun3() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

看一下Son1的结构:
在这里插入图片描述
可以看见,Son1本身还是没有vfptr,而是仍旧用从Base继承过来的vfptr,而且函数表也是只有一个,Son1的虚函数fun3的地址直接添加在虚函数表里面(在其他函数地址下面)。由此可见,一个类如果有虚函数,并且继承的有函数指针,那么这个类会继续用继承的vfptr,而且自己的虚函数地址会继续存放在同一个虚函数表中。但是不意味着每个类都只有一个虚函数指针和一个虚函数表,比如,两个类都有虚函数指针和虚函数表,又来一个"孙子"类继承这两个类,那么"孙子"类里面就会有两个虚函数指针,两个虚函数表。
如下代码:

class Base
{
public:
	virtual void fun1() {}
	virtual void fun2() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son1 :public Base 
{
public:
	virtual void fun3() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Son2 :public Base
{
public:
	virtual void fun4() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

class Grandson :public Son1,public Son2
{
public:
	void fun1(){}
	virtual void fun5() {}
public:
	int a;
protected:
	int b;
private:
	int c;
};

Son1和Son2的结构不用看了,上面已经分析过了,用继承的base的vfptr,虚函数地址和base的虚函数地址也存放在同一个虚函数表中。看一下Grandson的结构:

在这里插入图片描述
可以看见,Grandson从Son1和Son2分别d都继承了一份虚函数表,和分别都继承了一个虚函数指针。总共两个虚函数指针和两个虚函数表。而Grandson重写后的fun1函数将从Son1继承过来的虚函数表中的fun1覆盖了,而Grandson自己的虚函数fun5()也存放在从Son1继承过来的虚函数表中。在这里有一个规则:
1.虚函数地址按照其声明顺序放于虚函数表中。
2.父类的虚函数在子类的虚函数前面。
3.被重写的函数放到了虚函数表中原来父类虚函数的位置。即原来的虚函数的地址直接被覆盖。

发现从Son2继承过来的虚函数表中的fun1位置被
&thunk: this-=28;goto GrandSon::fun1替代
thunk我查了一下,说是一种thunk技术。thunk是一组动态生成的ASM指令,它记录了窗口类对象的this指针,这组指令既可以当作函数,也可以是窗口过程来使用。(如果真是这个知识的话,那么这里已经超出本人理解范围了)。猜测就是Son2的函数表里面的fun1并没有被覆盖,而是用了另一种技术调用Grand::fun1。this -= 28这句代码中,this应该是Son2的vfptr,而Son2的vfptr -= 28,那么就指向了Son1的vfptr,goto Grandson::fun1就是转到Grandson::fun1函数。
所以本人猜测,是通过回转到Son1的vfptr指向的虚函数表中找到Grandson::fun1函数的地址,然后调用。对于thunk,在其中不知道发挥着怎样的作用,已经超出本人知识范围了,网上搜了一下,也看的迷迷糊糊的。
总之,是通过某种方式调用Grand::fun1,而并不是简单的覆盖Son2的fun1就是了。

总结:
1.如果基类有虚函数表,那么子类直接使用基类的虚函数表。且实例中虚函数表地址值存储顺序就是基类继承顺序。
2.继承类新增的虚函数排在第一个虚函数表中,且在基类虚函数后面。
3.子类重写父类的虚函数,只有按顺序第一个包含被重写的虚函数的地址的虚函数表中的原虚函数地址被覆盖,后面的虚函数表中如果也包含这个虚函数地址,那么就不是简简单单的覆盖,而是用更像是回调的方式调用重写后的函数。
注: 本博客中如有错误,欢迎指出一起讨论学习。本博客主要以查看类的内容的方式理解分析虚指针和虚表。查看通过代码讲解验证的博客,请点击此处查看优秀博客。

  • 65
    点赞
  • 167
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 26
    评论
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值