C++:多态(重写,多态原理、单继承和多继承的虚函数表)


1. 多态的相关概念和性质

概念:

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

多态的构成条件:

父类函数必须为虚函数,并且在子类函数中必须重写该函数。
必须要通过父类的指针或引用去调用虚函数。

  • virtual关键字所修饰的函数就是虚函数。
  • 所谓虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。这个过程其实就是一个狸猫换太子的过程。
  • 在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
  • final关键字:修饰虚函数,表示该虚函数不能再被继承,通俗来讲就是一旦在基类中加上final关键字,则在派生类中就无法被重写
  • override关键字:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。这个和final关键字刚好是相反的,他是在派生类中进行使用的。

纯虚函数和抽象类:

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

2. 虚函数重写时的两个例外

① 协变:即基类和派生类虚函数的返回值不同

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

通缩一点来讲协变就是父类虚函数返回父类的指针或引用,子类虚函数返回子类的指针或引用。

举个例子来说就是

class Base{
public:
	virtual Base* fun()
	{//.....}
};

class D{
public:
	virtual D* fun()
	{//.....}
};

像这种情况就是协变,当然协变不一定每次都返回的是自己的指针或引用,也有可能返回其他类中基类或派生类的指针或引用。

② 析构函数的重写:

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

并且我们要清楚的是构造函数是不能发生多态的,换句话说多态机制在构造函数中是不发挥作用的。

在构造函数和析构函数中调用虚函数不会呈现多态性。

虽然构造函数支持重写,但它的内部是不会呈现多态性的,通常都是父类调用父类的,子类调用子类的。因为在调用父类的析构函数时,通常都意味着子类已经被释放掉了,因此父类析构不会去调用子类中重写的方法,而是调用自己的方法。

3. 重载、重写(覆盖)、重定义(隐藏)

重载:函数名相同,参数列表不同并且在同一作用域就构成了重载,返回值可以相同也可以不同。

重写:就是狸猫换太子,在派生类的作用域中,其重写的虚函数必须和基类的虚函数的函数名、参数列表、返回值均相同(协变和析构函数除外)。

重定义:就是继承中的同名隐藏,当派生类中有一个函数和基类的函数名相同,不管参数是否相同,只要该函数不为虚函数,则他就是重定义(同名隐藏)。

如图所示:
在这里插入图片描述

4. 多态的原理

首先我们先来看一组代码。

class Base{
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun2()
	{
		//....
	}
private:
	int m_a_;
};

问题来了,sizeof(Base)的大小是多少?

在没学多态之前,可能我们会认为Base类的大小为4Byte,但是在学了多态之后,Base的大小就不是我们所认知的那样了,他的大小为8Byte(在32位操作系统下)。那这是为什么呢?

我们先来看一下调试窗口。

在这里插入图片描述
我们可以清晰的看到,在Base类实例化出的对象b中,在最前面多出了一个指针变量__vfptr,并且该指针变量中存储的值为我们所定义出的两个虚函数。(需要注意的是有的对象可能会将该指针放在最后边,这个和平台有关)。

多出来的指针__vfptr我们将其称为虚拟函数表指针,该指针指向一个虚拟函数表,该表中所存的值为我们所定义出的虚函数。

一个含有虚函数的类中都至少含有一个虚函数表的指针,基类如此,继承它的派生类也是如此,那么当在派生类中重写某个虚函数的时候,该虚函数表会发生怎么样的变化呢?

首先我们完善一下代码,如下:

class D : public Base
{
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun2()
	{
		//....
	}
private:
	int m_b_;
};

void test()
{
	D d;
	Base *b = &d;
}

然后运行起来看结果

在这里插入图片描述
通过调试窗口,我们可以发现b的虚表指针中指向的值被偷梁换柱换为D中所重写的虚函数了。

那么多态的原理其实就一目了然了,那就是在子类中重写的函数会覆盖掉虚函数表中所对应的函数,当然这有一个大的前提就是必须是父类指针或引用去调用虚函数

用图来解释就是:

①在没有D类继承之前
在这里插入图片描述
②在D类继承之后,并重写对应的虚函数之后
在这里插入图片描述
这就是实现多态的原理

所以才说多态其实就是一种狸猫换太子的做法,当我们在子类中对父类的虚函数重写的时候,就必须做到三同(即返回值相同,函数名相同,参数相同),这样才能蒙骗过父类,当父类去调用该虚函数的时候,就会访问虚表,然而虚表中存放的是子类的函数,所以就会转去调用子类中的重写的虚函数。

这里我们需要注意的是

多态的调用,不是在编译时确定的,而是在运行起来以后到对象的中取找的。

虚表中存放的是虚函数的指针,虚函数和普通函数一样,存在于代码段中

只要对象的类型不同,所定义的虚表就不同,因为不同类型的虚表指针所计算的对应步长是不相同的

静态绑定和动态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
    -动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

5. 单继承和多继承的虚函数表

5.1 单继承的虚函数表

还是拿代码来进行分析:

class Base {
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun2()
	{
		//....
	}
private:
	int m_a_;
};

class D : public Base
{
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun3()
	{
		//....
	}
	virtual void fun4()
	{
		//....
	}
private:
	int m_b_;
};

void test()
{
	Base b;
	D d;
}

我们实现在D类中只实现了对Base类中虚函数fun1的重写,并且还加入另外两个虚函数,我们来看看调试窗口中它是怎样表示的。

在这里插入图片描述
观察上图中的监视窗口中我们发现看不见fun3和fun4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。
但是虽然在编译器中看不到,但我们必须清楚有这样的一个图
在这里插入图片描述

那么,我们该如何访问到fun3和fun4呢?

思路:取出b对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

我们可以将当前对象b强转为 (int*) &b并对其解引用,由于是int *,那么它指向的就是一个4Byte大小的内存空间,这个空间刚好是__vfptr指针所在的空间,然后我们再将__vfptr所在空间强转为 int *,并对其进行+0再解引用,这样我们这个指针就指向了第一个虚函数的地址,最后我们用typedef出来的函数指针,将其进行强转之后再调用,就可以调用对应函数了。

typedef void(*Pfun)()

Base b

1. *(int* )(&b) 
2.  * (int* )(*(int* )(&b)) + 0
3. (Pfun)(* (int* )(*(int* )(&b)) + 0)() -->调用fun1函数

(Pfun)(* (int* )(*(int* )(&b)) + 2)() -->调用fun3函数
(Pfun)(* (int* )(*(int* )(&b)) + 3)() -->调用fun4函数

5.2 多继承的虚函数表

继续看代码:

class Base1 {
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun2()
	{
		//....
	}
private:
	int m_a_;
};

class Base2 {
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun2()
	{
		//....
	}
private:
	int m_b_;
};

class D : public Base1,public Base2
{
public:
	virtual void fun1()
	{
		//....
	}
	virtual void fun3()
	{
		//....
	}
	virtual void fun4()
	{
		//....
	}
private:
	int m_c_;
};

调试结过如下:

在这里插入图片描述
我们可以得出一个结论:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

如下图:

在这里插入图片描述

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值