C++进阶:多态(七)

54 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

五、多继承的虚函数表

💦 多继承中的虚函数表

✔ 测试用例一:

#include<iostream>
using namespace std;

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

typedef void(*VFPTR)();
void PrintVFT(void* vft[])
{
	printf("%p\n", vft);
	for (size_t i = 0; vft[i] != nullptr; i++)
	{
		printf("vft[%d]: %p->", i, vft[i]);
		VFPTR f = (VFPTR)vft[i];
		f();
	}
	printf("\n");
}

int main()
{
	Derive d;

	PrintVFT((void**)(*(int*)&d));//打印Base1的虚表
	PrintVFT((void**)(*(int*)((char*)&d + sizeof(Base1))));//打印Base2的虚表,这里需要从起始位置跳过Base1个字节,这里跳时,要按1个字节跳,所以要先对起始位置强转
	
	//重写两次不多余,因为你要实现各自的多态
	Base1* p1 = &d;
	p1->func1();
	Base2* p2 = &d;
	p2->func1();

	return 0;
}
  • 这里有两个基类 Base1 和 Base2,这两个类中都包含两个相同的虚函数 func1 和 func2,Derive 继承了 Base1 和 Base2,并对 func1 进行重写,还新增了一个虚函数 func3。

    那么大概率猜测 Derive 对象中有两张虚表,因为如果混在一起,太麻烦了。其中多继承派生类的未重写的虚函数放在第一个继承基类 Base1 的虚函数表中。多继承中的重写会对两个基类重写。

    在这里插入图片描述

    重写两次有必要吗 ❓

      当然有必要,因为你要实现各自的多态,所以需要独立开来,虽然调用的都是同一个函数,如果是单继承,那么重写一次是没问题的,如果是多继承,那么只重写一次是有问题的,就类似过年回家要买礼物时,你不可能只给爸爸或只给妈妈买。

    Base1 和 Base2 中的 func1 都被重写了,它们都是 Derive::func1,但是地址却不一样 ❓

      这个不需要太在意,虽然地址不同,但是最终调用的是同一个函数,你可以认为虚表中不是它真正的地址,它在真正的地址上还包了一层,至于为啥要包就无从得知了。

    在这里插入图片描述

    简单看下汇编,注意不同的编译器实现的可能大同小异

    在这里插入图片描述

💦 菱形继承、菱形虚拟继承中的虚函数表

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面实际中用起来它的底层非常的复杂,我们之前是也仅仅是用成员变量做测试模型;另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用,我们这里就了解下在继承中说的虚基表中空的位置,其实这里虚基表中头上 4 个字节是找虚函数表指针的偏移量。

在这里插入图片描述

✔ 测试用例二:

#include<iostream>
using namespace std;

class A
{
public:
	virtual void func1()
	{}
public:
	int _a;
};
//class B : public A
class B : virtual public A
{
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
public:
	int _b;
};
//class C : public A
class C : virtual public A
{
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
public:
	int _c;
};
class D : public B, public C
{
public:
	virtual void func1()//重写B、C
	{}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	return 0;
}
  • 如果 B 和 C,都不重写的话,是可以正常运行的。但如果 B 和 C,都完成重写的话,D 一定需要重写,因为 D 如果不重写,就会存在歧义 —— 重写不明确,因为 D 继承了 B、C,虚继承解决二义性后,B、C 都没有放 A 了,而是把 A 放到了公共的最下面,A 中又带一个虚表,现在 B 和 C 对 A 中的 func 重写,D 继承之后,到底是用 B 的重写还是 C 的重写这是有歧义的。所以 D 也应该重写。

    在这里插入图片描述

  • 如果 B、C 单纯是重写 A 的,那么 B、C 中不需要虚表,但是 B、C 新增自己的虚函数,那它就得有单独的虚表,因为这时 B、C 共享的是公共的 A,如果把它们的虚函数往公共的 A 中放就不合适了。

    此时我们内存对象模型如下:

    验证一下:

    在这里插入图片描述

如果感兴趣,可以去看下面的两篇链接文章。

C++ 虚函数表解析

C++ 对象的内存布局