什么是多态?简单说就是完成某个动作时不同对象会产生不同的状态。见人说人话,见鬼说鬼话。
多态是建立在继承基础上的
实现多态的两个条件:
1.在基类中定义虚函数(virtaul),并在派生类对其重写。
2.用基类对象的指针或引用调用虚函数。
满足这两个条件时,通过传递不同类型的对象就会调用对应的虚函数,也就完成了多态的形式。
以下是对上面多态两个条件的解释:
1.虚函数即用virtaul关键字修饰的成员函数,这里要注意是成员函数,即不可以是友元函数或是普通函数,虚函数的概念只存在于类作用域中。那么若在基类中将一个成员函数设置为虚函数,在子类中必须将其重写,否则就没有将它设置成虚函数的意义。
重写的意思是在派生类中将基类中继承到子类的同名虚函数函数体重新定义,以此完成不同的功能,这里同名必须是子类重写的意思是在派生类中将基类中继承到子类的同名虚函数函数体重新定义,以此完成不同的功能,这里同名必须是子类与基类的返回值、函数名、参数列表完全相同,缺一则不构成重写。在子类中的重写可以不加virtual关键字修饰,但不建议这么做。若基类中的成员函数没有virtual继承到子类并对其函数体进行重新定义则也不构成重写,这里只是基类和子类的同名隐藏(函数名相同即构成同名隐藏)。通过这我们可以看出构成重写的要求更加严格。
重写对函数体内的内容是否修改并无要求,但一般都是要与基类中函数体内容不同,否则重写的意义就不存在了。
2.为什么要用基类的指针或引用调用虚函数呢?
在编译阶段无法知道使用哪个类的虚函数,只有在程序运行时通过实参的类型确定调用哪一个类的虚函数。
若不用基类的指针或引用作为实参而是按照值的方式进行传参,那么在编译时就会生成一个基类的临时对象,所以无论传递哪个类的对象调用的都是基类的虚函数。则不构成多态。
下面是一个简单的实现多态的代码:
两个对象分别调用了自己类中的TestFunc函数。

class B
{
public:

    virtual void TestFunc()
    {
        cout << "B::TestFunc();" << endl;
    }
    int _b;
};

class D : public B
{
public:
    virtual void TestFunc()override
    {
        cout << "D:TestFun();" << endl; 
    }
    int _d;
};

void Func(B & b)
{
    b.TestFunc();
}

int main()
{
    B b; 
    Func(b);
    D d;
    Func(d);
    system("pause");
    return 0;
}

虚函数重写的两个例外:(以下两种方式也构成重写)
1.协变(基类与派生类虚函数返回值类型不同):基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时

class B
{
public:
    virtual B * TestFun()
    {
        cout << "B::TestFun()" << endl;
        return new B;
    }
};

class D : public B override
{
public:
    virtual D * TestFun()
    {
        cout << "D::TestFun()" << endl;
        return new D;
    }
};

void Test(B & b)
{
    b.TestFun();
}

int main()
{
    B b;
    Test(b);
    D d;
    Test(d);
    system("pause");
    return 0;
}
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

C++11中override关键字写在派生类虚函数后检测这个函数是否是子类中虚函数的重写,没有则编译报错。final关键字修饰虚函数表示该虚函数不能再被继承。


在设计问题时有时在基类中实现一个函数不能具体下来,所以基类中的这个虚函数就无法实现,只有在子类中才能确定下具体的方法,所以我们需要创造一个纯虚函数来方便在子类中实现。纯虚函数即在虚函数的后面写=0,包含纯虚函数的类叫做抽象类,抽象类不能创造对象,派生类继承后也不能创造对象,只有对纯虚函数重写后派生类才能创造出对象,纯虚函数规范了派生类必须重写,体现了结构继承。但可以创建抽象类的指针。

看看下面的例子:

class WC//厕所类,分别让男上男厕所,女上女厕所
{
public:
    void GoToManRoom()
    {
        cout << "go to left" << endl;
    }

    void GoToWoManRoom()
    {
        cout << "go to right" << endl;
    }
};

class Person//这个类中没有给定性别
{
public:
    virtual void GotoWC(WC wc)=0;//没有性别就无法进行上厕所操作,所以将这个虚函数给成纯虚函数,在派生类中对其重写
    string name;
    int age;
};

class Man : public Person//男人类
{
    virtual void GotoWC(WC wc)//对继承下来的上厕所方法进行重写
    {
        wc.GoToManRoom();//调用GoToManRoom()
    }
}
class Woman : public Person//女人类
{
    virtual void GotoWC(WC wc)//对继承下来的上厕所方法重写
    {
        wc.GoToWoManRoom();//调用.GoToWoManRoom()
    }
};

#include
#include

int main()
{
    WC wc;
    srand(time(nullptr));
    Person *p;//创建一个人类的指针
    int i = 0;
    while (i < 10)
    {
        if (rand() % 2 == 0)
        {
            p = new Man;
        }
        else
        {
            p = new Woman;
        }
        p->GotoWC(wc);//让不同的人去上不同的厕所
        Sleep(1000);
        delete p;
    }
    system("pause");
    return 0;
}

输出结果:
多态_第1张图片


多态的原理:

以下说明都是在32位编译环境下在VS2013的测试
一、虚函数表
假设一个类中只有一个成员变量和一个虚函数,那么这个类的大小是多少呢?通过sizeof可以看到是8个字节,那么再增加一个虚函数呢?计算节后还是8个字节多态_第2张图片
通过监视窗口可以看到这多出来的四个字节存放的是一个虚表指针,一个含有虚函数的类中至少含有一个虚表指针,因为虚函数的地址存放在虚表中,通过虚函数指针才能访问到虚表。虚函数才会存在于虚表中。

派生类中又是怎么样的呢?我们继续往下看:

多态_第3张图片
首先要明确的是派生类与基类的虚表指针不同,D类继承自B类并对TestFun1()函数进行了改写,并新增加了TestFun5()和TestFun5()两个函数,所以派生类的虚表中存放的是这样的内容:多态_第4张图片
所以我们得出结论:
1.派生类继承下基类的成员变量,新增派生类指针将基类虚表的内容拷贝一份,若在派生类中重写某个虚函数,则用重写后的函数将之前的对应的虚函数替换(相同偏移量),覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法,没有重写的虚函数不做修改,派生类新增的虚函数按照生命顺序依次放到虚表的后面。
2.虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
3.注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的

二、多态的原理
多态_第5张图片
通过函数TestFun3()函数的反汇编代码,通过它我们可以看到多态到底是怎么工作的,首先前两个语句是从对象中的前4个字节取虚表的地址,第四条语句通过ecx寄存器传递this指针,第五条语句是从虚表中通过偏移量找到对应的虚函数,第六条语句就是调用虚函数了。所以通过传递不同的对象(this指针不同)就可以找到对应的虚函数,即实现了多态的过程。

还要注意满足多态的函数是在运行时确定对象并实现调用的,而不满足多态的函数是在编译时就确定好的。


基类指针指向派生类对象:
is-a的关系使基类指针可以指向派生类对象,但是在这个过程中基类指针只能访问基类的成员变量和成员函数,若想访问子类中的成员变量和成员函数则需要强制类型转化但是是一种潜在的危险操作,注意:如果在基类和派生来中定义了虚函数(通过继承和重写),并通过基类指针在派生类对象上调用这个虚函数,则实际调用的是这个函数的派生类版本。