【C++】多态(二)--单继承和多继承的虚函数表

在上篇博客 多态(一)中,我讲到了多态的概念及实现,虚函数的重写,抽象类等概念,这里我就继续讲多态的原理,单继承和多继承的虚函数表,及面试中常见的问题,前面的内容大家可点击链接去看哦。

5.多态的原理

一般在说到多态的原理的时候,我们都要讲到以下两点

1)虚函数表
2)动态绑定

5.1 虚函数表

首先看下面的代码,大家算一下sizeof(b) 的结果是多少?

class Base
{
public:
	virtual void func1() { cout << "func1" << endl; }
private:
	int _b = 1;
};
int main()
{
     Base b;
	cout << sizeof(b) << endl;     
	system("pause");
	return 0;
}

通过编译我们发现这里计算的结果是8。这是为什么呢?我们前面涉及计算类对象大小时都是除了空类是1,其他类都只计算成员变量的大小并对齐即可。那么这里有什么不同呢?
通过调试的监视窗口我们发现,b对象中除了 _b成员,还多了一个_vfptr在对象里面。这里的 _vfptr称为虚函数表指针 与关键字virtual 有关,也就是说一个含有虚函数的类中至少有一个_vfptr(虚函数表指针)指向虚函数表(虚表) ,此时的sizeof当然就变成8了。那么派生类中的虚表存放了什么呢?
我们继续看下面的代码:

//我们增加一个派生类去继承Base,在派生类中重写func1
//基类Base中增加一个虚函数和一个普通函数
class Base
{
public:
	virtual void func1() { cout << "func1" << endl; }
	virtual void func2() { cout << "func2" << endl; }
	void func3() { cout << "func3" << endl; }
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void func1() { cout << "Derive" << endl; }
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	cout << sizeof(b) << endl;      //8
	cout << sizeof(d) << endl;      //12
	system("pause");
	return 0;
}

通过观察和测试,我们发现下面几个问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分组成,一部分是父类继承下来的成员,另一部分是自己的成员。
  2. 基类b对象和派生类d对象的虚表不一样,因为派生类重写了基类的func1,所以d的虚表中存的Derive::func1(虚函数重写即覆盖)
  3. 另外,func2继承下来仍然是虚函数存放在虚表,而func3继承下来也是普通函数所以不在虚表内。
  4. 虚函数表本质是一个存储虚函数指针的指针数组,这个数组的最后放了一个nullptr。
  5. 总结派生类虚表的生成

1)将基类的虚表内容复制一份到派生类虚表中
2)如果派生类重写了基类的某个虚函数,则直接覆盖掉基类的这个虚函数
3)派生类自己新增的虚函数按声明顺序增加到派生类的虚表中

  1. 那么这里有人会好奇虚函数存在哪?虚表存在哪?
    答:这里的对象中存的是虚表的指针,虚表中存的是虚函数的指针,而虚函数和普通函数一样都是函数,所以虚函数存在代码段,虚表也存在代码段
  2. 同一个类的不同对象的虚表是同一张表。因为同一个类的虚函数是相同的即虚函数指针相同,此时虚表一定相同。
5.2 多态的原理——动态绑定

我们拿 多态(一)中买票的例子来说,那时我们发现构成多态的函数在调用时与对象有关
在这里插入图片描述

1)当func函数的参数p指向Person的对象时,p在Person的虚表中找到的虚函数是Person::BuyTicket
2)当func函数的参数p指向Student的对象时,p在Student的虚表中找到的虚函数是Student::BuyTicket。
3)这样就实现了不同对象去完成同一行为时,产生不同的形态。

那么,为什么达到多态必须要满足两个条件(虚函数覆盖和对象的指针或引用调用虚函数)呢?
这里我们通过汇编代码了解到:构成多态的函数在运行时才去相应的对象中拿到虚表指针,从而找到对应虚函数去完成调用;而不构成多态的函数则是在编译时就已经确定了要调用函数。
从而引出下面两个概念—— 动态绑定与静态绑定

  1. 静态绑定,又称前期绑定(早绑定),是在程序编译期间就确定了程序的行为,直接调用函数,也称静态多态。比如 函数的重载
  2. 动态多态,又称后期绑定(晚绑定),是在程序运行期间根据拿到的对象去确定程序的具体行为,才去调用具体的函数,也称动态多态。
    在前面买票的例子中就演绎了静态(编译时)绑定和动态(运行时)绑定。
6.单继承和多继承关系的虚函数表
6.1 单继承中的虚函数表
class Base   
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "func2" << endl; }
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "fun3" << endl; }
	virtual void func4() { cout << "fun4" << endl; }
private:
	int _d = 2;
};

在这里插入图片描述
通过监视窗口我们发现看不见Derive的func3和func4函数,这时什么原因呢?
其实是编译器故意隐藏了这两个函数,因为我在多态(一)中也讲过虚函数的主要意义就是被重写,而这里的Derive类没有其他派生类,所以func3和func4函数没有太大意义。但func3和func4函数是存在的,我们可以通过打印虚函数表中的函数看到。如下:

typedef void(*VFPTR)();
void PrintVTable(VFPTR VTable[])
{
	cout << "虚表地址" << VTable << endl;
	for (int i = 0; VTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数的地址:0x%x,->", i, VTable[i]);
		VFPTR f = VTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	VFPTR *tb = (VFPTR*)(*(int*)&b);    //Base的虚表地址
	PrintVTable(tb);
	VFPTR *td = (VFPTR*)(*(int*)&d);     //Derive的虚表地址
	PrintVTable(td);
	system("pause");
	return 0;
}

在这里插入图片描述
打印虚函数地址的思路:

1)取出b,d对象的头4个字节即虚表指针
具体做法将对象地址取出强转成(int*)的指针,再解引用取值时就是头四个字节的值,这个值就是指向虚表的指针
2)将值强转成(VFPTR*),因为虚表是存VFPTR类型(虚函数指针)的指针数组
3)虚表指针传递给PrintVTable()函数进行打印虚表(nullptr是虚表结束标志)
4)注意:这里程序如果崩溃不是代码的问题,而是编译器的问题因为它对之前虚表的处理不干净,我们只需要生成-清理解决方案,再编译运行就可以了

6.2 多继承中的虚函数表
class Base1
{
public:
	virtual void func1()  { cout << "B1:func1" << endl; }
	virtual void func2()  { cout << "B1:func2" << endl; }
private:
	int b1;
};
class Base2
{
public:
	virtual void func1()  { cout << "B2:func1" << endl; }
	virtual void func2()  { cout << "B2:func2" << endl; }
private:
	int b2;
};
class Derive :public Base1, public Base2
{
public:
	virtual void func1()  { cout << "D:func1" << endl; }
	virtual void func3()  { cout << "D:func3" << endl; }
private:
	int d;
};
typedef void(*VFPTR)();
void PrintVTable(VFPTR VTable[])
{
	cout << "虚表地址" << VTable << endl;
	for (int i = 0; VTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数的地址:0x%x,->", i, VTable[i]);
		VFPTR f = VTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
		VFPTR *t1 = (VFPTR*)(*(int*)&d);         //Derive中Base1的虚表地址
	PrintVTable(t1);
	VFPTR *t2 = (VFPTR*)(*(int *)((char*)&d + sizeof(Base1)));   //Derive中Base2的虚表地址
	PrintVTable(t2);
	system("pause");
	return 0;
}

在这里插入图片描述
由运行结果可知:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

7. 多态常见的面试题6以及参考回答
  1. 什么是多态?
  2. 什么是重载,重写(覆盖),重定义(隐藏)?
  3. 多态的实现原理?

答:虚函数表+动态联编

  1. inline函数可以是虚函数吗?

答:不能。因为inline函数没有地址,无法存放在虚函数表中。

  1. 静态成员可以是虚函数吗?

答:不能。因为静态成员函数没有this指针,无法用类名::成员函数的调用方式访问虚函数表,也就无法放入虚函数表。

  1. 构造函数可以是虚函数吗?

答:不能。因为虚表要在编译时创建,但此时构造函数还未生成虚表指针。

  1. 析构函数可以是虚函数吗?什么情况下是?

答:可以。并且最好把基类的析构函数定义成虚函数。(具体参考多态的定义)

  1. 对象访问普通函数和虚函数哪个更快?

答:如果是普通对象,两者一样快。如果是指针或引用对象,则调用普通函数更快,因为虚函数要到虚函数表中找到才能调用。

  1. 虚函数表是什么阶段生成的?存在哪里?

答:虚函数表在编译时生成, 一般情况下都存在代码段。

  1. 什么是抽象类?抽象类的作用是什么?

答:抽象类就是含有纯虚函数的类。它的作用是强制重写虚函数,另外抽象类也体现了接口继承关系。

  1. 多态中虚函数表和菱形继承中虚基表的区别?

答:虚函数表中存储了虚函数的指针,可通过指针指向的地址找到虚函数。
虚基表中存储相对偏移量,通过虚基表指针与偏移量的计算得到需要的地址。

之后还会更新内容,记得关注我博客哟~

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值