C++ 多态原理剖析A-6

多态的两个条件:虚函数重写。基类用引用或者指针来调用虚函数。本文旨在从逻辑和存储角度来记录学习


在了解多态前,我们想子类和父类应该保持一个怎样的关系呢?

(1)父类和子类是亲密的。因为他们有共性的部分。子类由继承父类而来,但对父类的变量和函数有一定的选择继承,和修改。比如对于买票这一个事件时,爸爸买成人票的票价与儿子买儿童票的票价不同。这里我们更想把买票这件事看成一个共有的模板。所以买票我们还是写成一个函数,也称之为接口。这个十分类似与我们说的分段函数f(x).对于儿子与父亲两个不同类型的实体,实现各自的逻辑。

  (2)父类和子类又是独立的。因为他们有差异的部分。与父类相比,子类总是有自己特有的函数或变量。这些只对子类开放。我们不希望父类干扰到子类,而是想父类行使一套法则,子类行使一套法则。但是这个票价不一样,我们还是要让这个接口 实现的不同的结果。而多态的出现就是为了解决这个问题,为不同数据类型的实体提供统一的接口,使不同的对象面对同一个函数时,做出各自的执行操作。

这两条是多态的动机,也是对虚函数及虚函数表指针,语法上较为自然的理解。


那么为了体现这个父子类的共性和差异,我们保留父子类该函数的继承关系,并做以标记【虚函数概念引入与重写】;同时我们在函数的实现上我们要区别对待,根据多态动态的做以改变虚函数表及虚函数表指针】

【虚函数概念引入与重写】

1.为啥要有虚函数?(接口继承与实现继承)

普通函数的继承是一种实现继承,子类继承了基父函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

2.虚函数:即被virtual修饰的类成员函数称为虚函数。

虚函数重写:子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型函数名字参数列表完全相同),称子类的虚函数重写了基类的虚函数。

纯虚函数与抽象类:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。

抽象类不能实例化出对象故派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Father
{
public:
    virtual void func() = 0
};

3.代码例1:

class Father
{
public:
	virtual void func() {  //在函数返回值类型前加一个virtual
		cout << "class->Father" << endl;
	}
};
class Son :public Father
{
public:
	virtual void func()override
		//重写 在函数参数后面加override,会检查是否构成重写条件
	{
		cout << "class->Son" << endl;
	}
	void func()override          
	{
		cout << "Can  be ovrride" << endl;
	}//can:说明子类重写可以省略 virtual关键字

	virtual int func() override  
	{
		cout << "Can  be ovrride" << endl;
		return 1;
	}//can  not:返回值类型不同
	virtual void func(int) override 
	{
		cout << "Can  be ovrride" << endl;
	}
	//can  not:参数列表不同
};
int main()
{
	Father b;
	Son d;
	system("pause");
	return 0;
}

【虚函数表及虚函数表指针】

首先通俗的理解下:多态是统一接口的,但是父类函数和子类函数的调用是独立的,所以要在各自的虚表中完成。

2.对”表“这个概念,一般理解为数组,这样,我们的联系实例对象要想找到这张虚函数表就要借助指针(当然引用也可以),虚函数表也简称虚表,这个指向表的指针我们成为虚函数表指针

 我们在调试状态下,看监视窗口,观察例1:

反映出信息:

1.子类和父类指向的虚表是不相同的,即使子类没有对父类的虚函数进行重写!,并且子类的func的类域不是父类的。

2.多了一个指针,也就是说明,占用了4个字节(32位平台下)


通过上面的介绍,我们从宏观的角度理解了到我们是如何实现多态的。那么下面我们进一步从微观角度 探索虚函数和虚表的存储过程,分析多态的实现。

代码例2:

class Base
{
public:
	void func() {  //在函数返回值类型前加一个virtual
		cout << "class->Base" << endl;
	}
	virtual void f1(){
		cout << "Base::f1()" << endl;
	}
	virtual void f2(){
		cout << "Base::f2()" << endl;
	}
	virtual void f3(){
		cout << "Base::f3()" << endl;
	}
private:
	int   _b;

};

class Derived :public Base
{
public:
	virtual void f1()//重写Base
	{
		cout << " Derived::f1()" << endl;
	}
	virtual void f3()//重写Base
	{
		cout << "Derived::f3()" << endl;
	}
	virtual void f4()//新定义的函数 
	{
		cout << "Derived::f4()" << endl;
	}
	virtual void f5()//新定义的函数
	{
		cout << "Derived::f5()" << endl;
	}
private:
	int  _d;
};

交代:

Base函数:有虚函数f1(),f2(),f3(),和普通成员函数f0();_b是基类的实例对象

Derived函数:重写了Base虚函数f1(),f3(),继承了虚函数f2(),新增了虚函数f4(),f5();_d是派生类的实例对象。

观察可以得出结论:

白色标记:_vfptr不同,说明父类和子类指针指向不同的虚函数表。

红色标记:函数地址相同,f2() 是没有被重写的虚函数,通过父类的指针调用f2()函数。

这说明,多态函数的调用与类无关,而与父类或子类的指针有关。 

(1)关于存储:对象里存的是虚表指针,通过虚表指针找到虚表(VS编译器下存在代码段里),虚表存的是虚函数指针(也就是虚函数地址),而虚函数和普通函数一样存在在代码段,通过call函数地址来调用函数!

(2)定性分析虚表:   虚函数表里面存了虚函数地址. 址的表征是指针,也就是说,虚表的本质就是一个指针数组,最后一个指针为null!既然是虚表,里面存的是虚函数的地址,无论是自己定义的,继承来的,重写父类来的,只要是虚函数地址统统都会列在虚表里,而至于普通成员函数地址则放在代码段通过call函数名来调用。  

(3)那么,基类实例对象_b调用f1(),和派生类实例对象_d调用f1()效果有什么不同呢?首先,_d调用f1(),结果应该是调用了派生类的f1(),那么基类的f1()消失了么?答案是没有,派生类重写了基类的f1(),也可以说是派生的类的f1(),在编译器判断其符合多态后,将原来复制过来的基类f1()覆盖掉了。这也说明,对于基表来说,先将继承下来的基类的虚表拷贝一份,再去判断是否构成派生类重写。最后把派生类的虚函数按声明顺序添加在后面。

(4)观察新增派生类,在监视窗口里,我们看不到新添加的派生类,但是我们可以通过“打印”这个方法模拟。


现在,我们来总结一下多态的条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

看了单继承的虚表,我们再进一步考虑下多继承的多态?挖个坑,后序填充~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘敬_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值