内不欺己,外不欺人。———孔子
有趣的多态
- 1、前言
- 2、概念
- 3、多态定义与产生条件
- 4、多态的重要组成成员-(虚函数)
- 5、虚函数的重写(覆盖)
- 6、辅助关键字override与final(了解即可)
- 7、重载,重定义(隐藏),重写(覆盖)
- 8、抽象类
- 9、多态的原理
- 9、1、虚函数表
- 9、2、多态原理
1、前言
在最开始,我会讲明白分那么多的小标题的目的就是方便不懂多态的人能够有大概的框架,知道多态的能够有目的的去复习。所以标题分那么多请不要见怪。多多包涵。
在这篇文章中已经讲过了C++中的一个重要的特性-继承,想回顾一下的可以点击一下链接,复习复习。接下来我们将进行多态的讲解,其中也有一部分内容和继承比较相似,容易搞混,所以我尽力的讲清楚多态的特点和注意点,如果是继承的问题想不明白的话,可以看看我之前的文章,其中也算是讲的比较详细的。
2、概念
多态,也可以理解是多种状态。不同的状态完成不同的事情,可能是目的一样,但是不同的对象,实现的结果却是不同的。
意思是类产生的对象(存在继承的关系),在实现函数的时候,调用的是同一个函数名,但是执行出来的结果却是大相径庭的。
3、多态定义与产生条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
比如:Student继承了 Person。Person对象买票全价,Student对象买票半价。
除此之外,想要实现多态还需要两个条件。
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
重写与继承中的隐蔽有区别或者说是重定义是不一样的!
在继承中,函数的重定义只需要函数名字相同就能够构成重定义,但是对于多态的实现必须要三个要素都相同函数名,参数,返回值。
其中不管是基类还是派生类,在进行多态函数调用的时候,必须是用基类的引用或者是指针。
4、多态的重要组成成员-(虚函数)
和虚继承相似,虚函数的定义也会用到virtual关键字,但是关键字的位置是不一样的,怎么说呢,就好像是&的操作一样,在不同的场景之下,可能是取地址也有可能是引用,所以要注意区别,分别看待。
5、虚函数的重写(覆盖)
虚函数的重写就是在子类继承之后在其中有一个和父类三要素都相同的虚函数,称子类的虚函数重写了基类的虚函数。
注意: 在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl;
}
析构函数的重写,与众不同。析构函数即使不加virtual关键字也会重写,当然了,即使是名字不同也还是会重写。名字的不同是在我们编辑的时候定义的不同的名字,但是对于编译器来说,到编译的那一步的时候,已经将所有的析构函数统一名字了。编译后析构函数的名称统一处理成destructor
6、辅助关键字override与final(了解即可)
override与final关键字能够很好的帮助我们检查多态中函数重写可能存在的问题。
**override:**检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
final: 修饰虚函数,表示该虚函数不能再被重写
7、重载,重定义(隐藏),重写(覆盖)
三种概念实现的东西,相对而言是类似的,能够概括的认为,表面上是为了调用相同的东西,但是细微的区别能够又有着不同的处理方法,可能还会是不同的效果。
所以三种不同的操作,定义之间对于区别的掌握也是十分重要的。
8、抽象类
抽象类的形成就是在虚函数的最后加上“=0”,抽象类不能够实例化,如果继承的话,形成的派生类如果不重写虚函数也不能够实例化对象。所以抽象类的出现,一定程度上要求严格了虚函数重写,因为不重写就不能实例化出对象,也就没有下面之后的事情。同时抽象类也称为纯虚函数(我个人看到这个名词就感觉像是不能产生对象的意思)。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
另外,纯虚函数体现了接口的继承观念。
9、多态的原理
9、1、虚函数表
就像是在继承中的虚继承一样,其中作为基类的A在内存中存放的位置是在最底下,但是同样的,为了保证是相同的a,B和C中在内存中的第一个地址的位置存放着虚基表来帮助找到a的位置,能够修改和得到a的值。这篇文章中还有图的解释,这里就不再多赘述了。
**有趣的是,多态中的原理和虚继承好像还有点相似,**其中多态中,有着另一种表虚函数表。为了更能够理解虚函数表的作用,我们先从一个题目来入手。
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
答案是8byte而不是只有4个byte,那么多出来的4byte是什么?那其实是一个指针,一个虚函数表指针。当然了,如果一个基类中有多个虚函数的话,只会存在一个虚函数表指针,也就是意味着在刚刚的Base类中,即使还有别的虚函数,最后Base的大小也还是4byte。虚函数表指针简称也叫做虚表指针。
但是,如果是多继承下来的子类中的虚函数表可能不止一个。
编译器,实现多态的方法是靠在第一个元素位置的地址指向的地址,调用不同的函数。
满足多态的条件:那么这里的调用生成的指令,就会去指向对象中的虚表中找对应的函数进行调用。
不满足多态的条件:直接就确定函数的地址,而不去虚函数表里面找,因为直接找到了。
为什么重写也称为是覆盖呢?因为在地层中,重写条件满足后,当前对象中的虚函数表就会改写,原本从父类继承下来的相同的虚函数就会换成子类中的函数指针的位置,这样的步骤看做为覆盖是十分合理的。
9、2、多态原理
了解了虚函数表之后,想一想在第三个标题中图片的内容,其实在底层的实现中也就是像这个样子的。
**虚表:**虚函数表,存的虚函数,目标实现多态。
**虚基表:**存的是当前位置距离虚基类部分的位置的偏移量,防止存在菱形继承的二异性问题。
Tip: 每一个存在的数据都会存放在计算机内部,而计算机拥有着。栈,堆,静态区,常量区。那么对于虚表来说,存放在哪呢?
由此可见,vs上的虚表是存在于常量区之中的。