C++ --- 什么是多态

多态的概念

多态就是当要完成某个行为,当不同的对象去完成时会产生不同的效果。比如:在火车站买票,普通成年人,需要全价买票,学生可以半价买票,军人可以优先买票。

条件:被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写;必须通过基类的指针或者引用调用虚函数。

注意点是:如果基类的函数不是虚函数,重写函数只是构成隐藏。

如果不满足多态条件:

1.不重写虚函数

 2.不是虚函数,重写

3.不用指针或者引用调用

重写

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数的返回值类型,函数名字,参数列表完全相同),但是函数的实现不同,称派生类重写了基类的虚函数。

虚函数的重写,需要函数名,参数,返回值类型一样。但是派生类只是继承了函数的接口,接口就是函数名,参数,返回值类型。派生类重写只是实现不同。

析构函数的重写

问题:

 所以析构函数需要定义成虚函数,来构成多态:

C++11里的override和final关键字

 final:修饰虚函数,表示该虚函数不能被继承,不能进行重写。

 修饰类,类不能被继承

 在C++98中,为了不让类被继承,可以将基类的构造函数私有化(private),于是派生类就不能构造属于基类的成员。

override 检查派生类虚函数是否重写了基类的虚函数,如果没有则编译错误。

这个只能修饰派生类的虚函数,不能修饰基类虚函数。

 override关键字最好用来检查派生类虚函数接口(函数名,参数,返回值)是否写错。

抽象类

在虚函数后面写上=0,这个函数称为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写的纯虚函数,派生类才能实例化出对象。

 只有派生类经过纯虚函数重写才能实例化出对象。但是基类还是抽象类,不能实例化对象

 纯虚函数的作用:

1.一定程度上强制了派生类对纯虚函数的重写,如果不重写,派生类就不能实例化对象。

2.表示抽象的类型

应用场景:

比如:比如,只说一辆车,车是抽象的,是一个很笼统的概念。因为有多车,不知道具体什么车,并且你不会拿车实例化对象,车这个类就可以定义成抽象类,里可以写成员函数,但是不写具体实现。

但是某个品牌的车,比如奔驰,可以继承车这个抽象类,只需要重写纯虚函数,就可以实例化对象。

具体的继承抽象的。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。只是可以有隐藏。

虚函数的继承是一种接口继承,派生类只是继承了基类虚函数的接口,目的是为了重写,达成多态。

所以如果不实现多态,不要定义成虚函数。

 多态的原理

虚函数表

 

通过观察我们发现,对象b是8个字节,除了_num外,还有_vfptr指针。这个指针是我们叫做虚函数表指针,简称虚表指针。一个含有虚函数的类中至少有一个这样的指针。

_vfptr指针变量保存的是虚函数表的起始地址。

虚函数表实际是一个函数指针数组,虚函数表简称虚表。虚表里面保存的都是虚函数的地址。

派生类中的虚表指针

派生类不重写基类的虚函数

 通过上面现象说明一个结论:派生类会继承基类的虚函数,会继承基类的虚表。但是派生类和基类的_vfptr变量内容不相等,说明两个虚表不是同一种虚表,只是虚表里的内容相同,所以会调用同一个函数。

 派生类重写虚函数

 通过上面现象说明一个结论:派生类重写基类虚函数,会重写派生类虚函数表里的内容,将对应位置覆盖层重写虚函数的指针。

派生类增加虚函数

 注意:

        1.类中有虚函数只是这个类中多了虚函数表指针,不是将虚函数表保存到类中。

        2.虚函数表最后会以nullptr结尾。

        3.同类型的对象共用一张虚表,可以理解成一个类的虚表属于这个类的,实例化的对象,都公用这一张虚表。

 总结派生类虚表的生成:

1.派生类会继承基类的虚表,当然两个虚表表不是一张虚表。派生类先将基表虚表的内容拷贝一份到派生类的虚表中

2.如果虚表重写虚函数,用派生类重写虚函数的地址覆盖掉虚表中对应虚函数的地址。

3.派生类增加虚函数,会在派生类虚表中声明次序增加到虚表的最后(不会新增虚表)。

虚表保存在哪

首先说明,一个具有虚函数的类_vfptr保存在前面还是后面是由平台决定的,根据上面的现象,我们平台_vfptr是保存在最前面的

多态原理

多态是基于虚函数的虚函数表。构成多态,跟对象有关。如果是基类对象,会去基类的虚表中找要调用虚函数的地址,去执行虚函数的代码。如果是派生类对象,会去派生类类的虚表中找要调用虚函数的地址,去执行虚函数的代码。

派生类虚函数重写之后,可以实现不同的对象,有不同的实现方法,展现不同的效果。

再来段代码理解一下:

 通过汇编分析,看出满足多态以后函数调用,不是在编译时确定的,是在运行起来后到对象的需表中找的。

不满足多态,是在编译时确定好的。

动态绑定和静态绑定

静态绑定又称为前期绑定,在程序编译期间确定了程序的行为,也称静态多态。比如函数重载。在编译的时候确定了调用的函数。
动态绑定也称后期绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称动态多态。就像上面的,运行时在到虚表中找调用函数的地址。
多态,多数都是动态绑定。

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

单继承中的虚表

 结论是否如上图所述:我们来打印一下虚表

打印代码

class Person 
{
public:
    virtual void func1(){ cout << "Person func1()" << endl; }
    virtual void func2(){ cout << "Person func2()" << endl; }
 
};
 
class Student :public Person
{
public:
    virtual void func1(){ cout << "Student func1()" << endl; }
    virtual void func3(){ cout << "Student func3()" << endl; }
protected:
 
};
 
 
typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR;
//打印代码
void PrintVfTable(VFPTR *vftable){
    for (int i = 0; vftable[i] != nullptr; i++){
        //打印虚表内容
        printf("vftable[%d]:%p\n", i, vftable[i]);
        //调用这个函数
        VFPTR fun = vftable[i];
        fun();
    }
    cout << endl;
}
 
int main()
{
    Person p;
    Student s;
    //要得到虚表指针的内容,由于这个平台,虚表指针是保存在开始的
    //先得到对象地址,强转成int *得到前四个字节,就是虚表指针的地址
    //再解引用,得到虚表指针的内容
    //再强转成函数二级指针
    PrintVfTable((VFPTR *)*(int *)&p);
    PrintVfTable((VFPTR *)*(int *)&s);
 
    getchar();
    return 0;
}

多继承中的虚表

 根据上面的结论可以得到这样一张图:

#include<iostream>
 
using namespace std;
 
class Base1
{
public:
    virtual void func1(){
        cout << "Base1 : func1()" << endl;
    }
    virtual void func2(){
        cout << "Base1 : func2()" << endl;
    }
protected:
    int _a;
};
 
class Base2
{
public:
    virtual void func1(){
        cout << "Base2 : func1()" << endl;
    }
    virtual void func2(){
        cout << "Base2 : func2()" << endl;
    }
    virtual void func3(){
        cout << "Base2 : func3()" << endl;
    }
protected:
    int _b;
};
//多继承
class Deirve :public Base1, public Base2
{
public:
    virtual void func1(){
        cout << "Deirve : func1()" << endl;
    }
    virtual void func4(){
        cout << "Deirve : func4()" << endl;
    }
    virtual void func5(){
        cout << "Deirve : func5()" << endl;
    }
protected:
    int _c;
};
 
 
typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR;
 
void PrintVfTable(VFPTR *vftable){
    printf("虚表地址:%p\n", vftable);
    for (int i = 0; vftable[i] != nullptr; i++){
        //打印虚表内容
        printf("vftable[%d]:%p\n", i, vftable[i]);
        //调用这个函数
        VFPTR fun = vftable[i];
        fun();
    }
    cout << endl;
}
 
int main()
{
    Deirve d;
    //打印继承Base1的虚表
    PrintVfTable((VFPTR *)*(int *)&d);
 
    //打印继承Base2的虚表
    //加Base1大小的字节数,到Base2的虚表指针。
    //要加先强转成char *,步长为一个字节。不强转的话,步长为Deirve
    PrintVfTable((VFPTR *)*(int *)((char *)&d + sizeof(Base1)));
    getchar();
    return 0;
}

 画图表示为:

 

posted @ 2017-09-02 11:38  流水灯  阅读(3322)  评论(2编辑  收藏  举报