多态是面向对象编程内极为重要的一种特征,它能极大的简化我们的代码

先来看一段类的原型

class Animal //基类
{
public:
    void voice()
    {
        std::cout << "Voiceless" << std::endl; //不知道该叫啥
    };
};

class Dog : public Animal //狗派生类,继承了Animal
{
public:
    void voice()
    {
        std::cout << "Woof" << std::endl; //汪汪叫
    }
};

class Cat : public Animal //猫派生类,继承了Animal
{
public:
    void voice()
    {
        std::cout << "Meow" << std::endl; //喵喵叫
    }
};

我们可以看到,基类和派生类中都有函数voice(),它可以让不同的动物发出不同的叫声,其中,Dog类和Cat类的voice()函数是覆写了基类中的同名函数。

当然,还可以有很多基于Animal类的派生类,都可以有叫声,但是我们如果想统一一下让它们叫的方法,比如用个train函数,传入对象或者它的指针就可以自动调用它的voice函数。虽然Cpp支持重载函数,但是为每个不同的类各写一个这样的函数,工作量不仅大,而且枯燥。多态就是帮我们解决这个问题的。

多态 Polymorphism

表现方式有两种

  • 重载多态,比如函数重载(本文不介绍了),属于静态联编
  • 子类型多态,属于动态联编,就是我接下来要说的那些

子类型多态有两个要点:

  1. 虚函数
  2. 函数覆写override

我们来区分涉及继承链情况下的两种联编:

  1. 通过派生类对象访问同名函数,是静态联编
  2. 通过基类对象的指针访问同名函数,是静态联编
  3. 通过基类对象的指针或引用访问同名虚函数,是动态联编

那我们现在改一下原来的类的定义让它符合子类型多态的要求

class Animal
{
public:
    virtual void voice() //此函数声明为虚函数后,派生类中无须再注明
    {
        std::cout << "Voiceless" << std::endl;
    };
};

class Dog : public Animal
{
public:
    void voice() override //覆写了voice函数
    {
        std::cout << "Woof" << std::endl;
    }
};

class Cat : public Animal
{
public:
    void voice() override //覆写了voice函数
    {
        std::cout << "Meow" << std::endl;
    }
};

那么还是刚刚那个例子,我们设计一个train函数

void train(Animal animal)
{
    animal.voice();
}

使用这个函数,你会发现无论你往参数里塞了什么阿猫阿狗的对象,输出的永远是“Voiceless”,这便是静态联编的问题了,它使用的是基类的对象而非基类对象的指针或引用,你需要把train修改成这样:

void train(Animal& animal)
{
    animal.voice();
}
//或者这样
void train(Animal* animal)
{
    animal->voice();
}

然后你的子类对象在调用这个train函数之后就可以正常发出正确的声音了

需要注意的是,如果voice函数不是虚函数,那么即使你用了派生类对象的指针、引用,只要那个进入函数的东西的类型是基类,它就会调用基类的函数

一句话总结一下,就是:函数虚,看对象;函数实,看类型。

但是,这个东西它也有问题,就是你使用基类的指针(或者引用)指向了派生类的对象的时候,你只能调用派生类的虚函数,无法调用非虚函数,例如我改一下Dog类的定义:

class Dog : public Animal
{
public:
    void voice() override
    {
        std::cout << "Woof" << std::endl;
    }
    void foo() //这个不是虚函数了
    {
        std::cout << "Foo!" << std::endl;
    }
};

但是我在main函数里这样定义:

Animal ptr;
Dog dog = new Dog();
ptr = &dog;
ptr->voice(); //调用的是dog对象的voice函数
ptr->foo(); //无法调用,No member named foo in Animal

Animal类的指针虽然指向了dog,但是它将无法调用dog的非虚函数,但是如果我们偏要调用属于子类的非虚函数呢?

Introducing dynamic_cast!

Cpp的dynamic_cast<>运算符可以把基类类型的指针转换为派生类的类型的指针,就可以使调用子类的非虚函数称为可能,方法如下

ptr->foo(); //它出了问题
dynamic_cast<Dog*>(ptr)->foo(); //正常调用!

虽然建议在任何基类与派生类间的指针、引用的类型转换都使用dynamic_cast运算符,但是实际上,派生类的指针(引用)转为基类类型的指针(引用)可以不使用该运算符而隐式转换,而基类的指针(引用)转为派生类类型的指针(引用)必须显式使用该运算符以完成类型转换。

比如我们有:

Animal animal;
Animal* pa;
Cat cat;
Cat* pc;

pa = &cat; //可以隐式转换(下转上)
pc = dynamic_cast<Cat*>(animal); //必须显式转换(上转下)

多态就先写这么多~