多态与虚函数(虚表)

前面我们已经说过继承,今天我们来说说多态,多态是什么呢?我们可以从以下几个方面来看多态。
1、什么是多态?
2、多态的分类?
3、动态多态实现的条件?
4、多态实现原理?
现在我们就要进入正文了!
1、什么是多态?

多态:同一事物,在不同场景下可表现出不同的形态。
例如:1.'*' 的多种意思
        a*b;//此时 '*'就是乘号的意思
        int* p=&a;
        *p=2;//此时 '*' 就是解引用的意思
     2.厕所
         如果男生来了就是男厕所,女生来了就是女厕所
     3.其实多态我们也可以这样理解:
         见人说人话,见鬼说鬼话

2.多态的分类
多态与虚函数(虚表)_第1张图片

多态与虚函数(虚表)_第2张图片

静态多态:发生在程序编译期间,编译器会根据函数实参的类型(可能会进行隐式的类型转换)可推断出要调用那个函数,如果有对应的函数就调用该函数,如果没有就出现编译错误
函数重载:
运行环境:vs2013,win10
#include
#include
using namespace std;
int Add(int left, int right)
{
    return left + right;
}
float Add(float left, float right)
{
    return left + right;
}
int main()
{
    cout << Add(1, 2) << endl;
    //执行此函数时,在编译期间,编译器会根据实参的类型取推断应该调用那个加法函数,此时实参的类型是int型,所以编译器会去调用int Add(int left,int right);
    cout << Add(1.1f, 2.2f) << endl;
    //执行此函数时,在编译期间,编译器会根据实参的类型取推断应该调用那个加法函数,此时实参的类型是float型,所以编译器会去调用float Add(float left,float right);
    cout << Add(3, 'd') << endl;
    //执行此函数时,没有对应的函数解决此类型的函数,所以编译器就会推断会应该调用那个函数(在编译期间),从而发生隐式的类型转换,把字符'd'转换为其所对应的ASCII码值100,然后调用int Add(int left,int right);
    system("pause");
    return 0;
}
为什么函数重载在编译期间可以确定应该调用哪个函数?
答:因为在编译期间,编译器会进行参数类型的推断

运行结果:
多态与虚函数(虚表)_第3张图片

动态多态:在程序执行期间(非编译期),编译期间只会编译程序,然后在执行期间才会决定调用那个函数.(在编译期间,就只是进行单纯的编译,比如:定义的是基类,编译的时候只会按表面定义的进行编译,只有在执行期间才会根据传参的不同,而选择应该调用那个函数)

3、动态多态实现的条件

动态多态实现的条件共有两个
1.基类中必须有虚函数,在派生类中必须要对基类中的虚函数进行重写;
2.通过基类的指针或引用来调用虚函数(派生类一定要重写基类的虚函数)

为什么?

把函数定义为虚函数,是为了实现多态调用,虚函数调用时会查询虚表
1.为什么构造函数不能作为虚函数?
   构造函数是创建对象的,如果构造函数是虚函数,调用构造函数时就需要查询虚表,可是对象还没构造完成,根本就不能查询虚表

2.为什么静态成员函数不能作为虚函数?
  不是类的成员函数,没有this指针

3.建议:做好不要将赋值操作符重载定义为虚函数,为什么?
  不能把派生类对象赋值完整,会让代码不安全,逻辑混乱(因为可能会让基类对象给子类对象赋值,不符合赋值兼容规则)
  赋值兼容规则规定:子类对象可以给父类对象赋值,可是基类对象不能给子类对象赋值

4.为什么友元函数不能定义为虚函数?
  不是类的成员函数,没有this指针

5.为什么最好将基类中的析构函数作为虚函数?
  如果不定义为虚函数,可能会使派生类中的资源清空不了。
6.为什么不要在构造函数和析构函数里调虚函数?
构造函数:
  对象不完整,可能会出现未定义的行为(虚表是通过对象调的,有些对象没有创建,使用此对象,可能会使程序崩溃)
析构函数:
   对象不完整,已销毁,再调用虚函数时,可能会查不到虚表(因为需要通过对象查询虚表)

4、动态多态实现的原理
虚函数

1.必须是类的成员函数;
2.使用关键字virtual修饰;
注:某一函数在基类中定义为虚函数,在派生类中重写时,可以不加关键字virtual,此时此函数也是虚函数,但是尽量加上关键字 virtual

纯虚函数

在类的成员函数(必须是虚函数)的形参列表后面写上=0;则此函数就是纯虚函数。
例如:
class Base
{
public:
   virtual void Test(int a,int b)=0;//此函数就是纯虚函数
}

抽象类

  包含纯虚函数的类就是抽象类(也叫接口类),抽象类不能实例化对象,纯虚函数在派生类中重新定义后,派生类才能实例化对象
  抽象类可创建指针
  抽象类的纯虚函数在派生类中如果没有被定义,则此派生类也是抽象类
  因为:抽象类定义不完整。抽象类它只是一个接口,不是一个真正的类,只有在派生类中给出功能之后,才可以实例化对象,就比如说一个厕所,它是一个模糊的概念,但是如果一个男生去厕所那它就是男厕所,这样它就有它自己的功能了。

虚表解析
以下是一个虚表
多态与虚函数(虚表)_第4张图片

单继承

基类虚表:虚函数是按照在类中的声明顺序排列的

派生类虚表:
1.先将基类虚表中的内容拷贝一份(内容一样,地址不一样)
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数
3.派生类中新增加的虚函数,按照其在派生类的声明顺序,将其放在上述的虚函数之后(即和基类共用一份虚表,而且派生类中的虚函数存储在基类虚表中基类的虚函数之后)

多态与虚函数(虚表)_第5张图片

多继承

  如果派生类的多个基类中均有虚函数,将派生类自己新增加的虚函数放在第一个“基类虚表”之后(其中细节的实现跟单继承一样,就是说基类的虚表和派生类在基类虚表中存储虚函数的实现与单继承一样)
第一个虚表:按继承先后顺序的第一个,
例如:class D:public C1,public C2//此时C1就是第一个基类

多态与虚函数(虚表)_第6张图片

C1和C2是派生类的基类

菱形继承

菱形虚拟继承派生类的虚表:
1.用来覆盖基类的虚函数;
2.放派生类新增加的虚函数(如果有虚函数,就会新增虚表,否则不会)

多态与虚函数(虚表)_第7张图片

多态与虚函数(虚表)_第8张图片

同一个类的多个对象共用一份虚表
一个类只会维护一个虚表

多态的缺点

降低效率
例如:
调某函数时,普通函数是直接调用
虚函数---->查找虚表的地址,然后再查找虚函数的地址,这样会增加程序调用时间,降低效率。

你可能感兴趣的:(C++)