面向对象的三大特性 - 多态与虚函数

多态(polymorphism)指的是不同类的对象对同样的消息可以做出完全不同的响应,即同一个消息可能会导致调用不同的方法。消息在 C++ 中指的就是函数的调用,不同的函数可以具有多种不同的功能,而多态就是允许用一个函数名的调用来实现不同的功能。

具体点就是去完成某个行为 (函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票 (5 折或 75 折);军人买票时是优先买票。

再比如,同样是动物叫的一个行为 (函数),传猫对象过去,就是喵喵喵,传狗对象过去,就是汪汪汪。

按照实现的时机,多态可以分为两类:

  1. 编译时多态:在函数名或运算符相同的情况下,编译器在编译阶段就能够根据函数参数类型的不同来确定要调用的函数 - 通过 重载 实现;
  2. 运行时多态:在函数名、函数参数和返回值类型都相同的情况下,只能在程序运行时才能确定要调用的函数 - 通过 虚函数(virtual functions) 实现

动态多态 是本章要讲述的内容。本教程后面提及的多态都是指运行时的多态。在 C++ 中,多态主要通过 虚函数(virtual functions)和继承来实现的。

父类指针指向子类对象

C++ 允许一个父类对象的指针指向其子类的对象,但是不允许子类指针指向其父类对象。

需要注意的是,这个父类指针指向子类对象后,通过该指针只能访问子类中从父类继承的公有成员,不能访问子类中新增加的成员,除非通过强制类型转换将父类指针转换为子类指针。例如:

#include <iostream>
using namespace std;

// 定义父类
class Father {
public:

    void work() {
        cout << "工作" << endl;
    }

};

// 定义子类
class Son: public Father {
public:

    void study() {
        cout << "学习" << endl;
    }
};

int main() {
    Father father, *ptr_father;     // 定义父类对象与父类指针
    Son son, *ptr_son;              // 定义子类对象与子类指针
    ptr_father = &son;              // 父类指针指向子类对象

    father.work();
    son.study();
    ptr_father->work();             // 通过父类指针访问子类对象从父类继承来的公共成员

    return 0;
}

需要注意的是,我们不能通过父类指针 ptr_father 访问子类自己定义的成员,错误用法如下:

ptr_father->study();        // study() 方法是子类自己定义的父类指针不可调用

如果想要解决以上问题,需要将强制类型转换将父类指针转换为子类指针。

以下是错误用法:

Son* ptr_son = &father; // 定义子类指针指向父类对象
ptr_son->work();

接着,我们在 Father 类与 Son 类中,都添加一个公共方法 show(),并且通过父类对象调用、子类对象调用、父类指向子类对象指针调用 show() 函数,代码如下:

#include <iostream>
using namespace std;

// 定义父类
class Father {
public:
    void show() {
        cout << "我是爸爸" << endl;
    }

    void work() {
        cout << "工作" << endl;
    }

};

// 定义子类
class Son: public Father {
public:

    void show() {
        cout << "我是儿子" << endl;
    }

    void study() {
        cout << "学习" << endl;
    }
};

int main() {
    Father father, *ptr_father;     // 定义父类对象与父类指针
    Son son;                        // 定义子类对象与子类指针
    ptr_father = &son;              // 父类指针指向子类对象

    father.show();
    son.show();
    ptr_father->show();             // 通过父类指针访问子类对象从父类继承来的公共成员

    return 0;
}

首先,father.show() 与 son.show() 打印出来的结果是毋庸置疑的,关键在于 ptr_father->show() 的结果

我是爸爸
我是儿子
我是爸爸

根据之前学习的内容,我们知道父类指针指向基类对象的只能调用父类中的公共成员,因此,子类自己创建的 show() 无法使用,而是直接调用父类中的 show() 方法。

如果我们可以通过使用父类指针来统一调用不同子类的成员,那么我们就实现了多态。但是,现在的问题是,这种方式,我们只能调用父类的成员,不能调用子类的成员。为了解决这个问题,我们就需要用到 虚函数

虚函数 - virtual functions

类成员函数前面加 virtual 修饰,那么这个成员函数被称为 虚函数。注意非成员函数不能加 virtual 修饰。

父类指针指向子类对象,通过 虚函数,可以调用子类的成员,同一个指针,可以调用不同对象的同名方法,执行不同的代码,对同一消息做出不同的响应,从而实现多态。

虚函数的定义格式如下:

class 类名 {

    virtual 数据类型 成员函数名 (参数列表);
}

声明虚函数说明该成员函数在子类中可能被重新声明,而且虚函数必须存在于类的继承环境中才有意义。

运行时多态的三个必要条件:

  1. 继承关系
  2. 虚函数重写
  3. 父类指针/引用调用

例如,我们创建一个父类 Father,两个子类 Son、Daughter,都添加 show() 公共方法,并将其设置为虚函数,代码如下:

#include <iostream>
using namespace std;

// 定义父类
class Father {
public:
    virtual void show() {
        cout << "我是爸爸" << endl;
    }
};

// 定义子类
class Son: public Father {
public:
    virtual void show() {
        cout << "我是儿子" << endl;
    }
};

// 定义第二个子类
class Daughter: public Father {
public:
    virtual void show() {
        cout << "我是女儿" << endl;
    }
};

int main() {
    Father father, *ptr_father;     // 定义父类对象与指针
    Son son;                        // 定义第一个子类对象
    Daughter daughter;              // 定义第二个子类对象

    ptr_father = &father;           // 指向父类对象
    ptr_father->show();

    ptr_father = &son;              // 第二次指向第一个子类对象
    ptr_father->show();

    ptr_father = &daughter;         // 第三次指向第二个子类对象
    ptr_father->show();
    return 0;
}

最后,我们让 ptr_father 分别指向不同的对象,再调用 show() 虚函数就可以获得不同的结果,从而实现了多态。

注意

构造函数不能定义为虚函数,但是析构函数建议定义为虚函数。只有定义为虚函数的析构函数,才能保证先释放子类再释放父类。

纯虚函数与抽象类

有时候,在父类中无法给出虚函数的实现代码,这时可以把虚函数声明为 纯虚函数。纯虚函数没有函数体,仅仅是起到一个提供同一接口的作用。

语法格式如下:

virtual 数据类型 成员函数名 (参数列表) = 0;

例如,上段代码中的 Father 类,我们如果把其中的 show() 函数内容清空并设置为纯虚函数,代码如下:

#include <iostream>
using namespace std;

// 定义父类
class Father {
public:
    virtual void show() = 0;
};

// 定义子类
class Son: public Father {
public:
    virtual void show() {
        cout << "我是儿子" << endl;
    }
};

// 定义第二个子类
class Daughter: public Father {
public:
    virtual void show() {
        cout << "我是女儿" << endl;
    }
};

int main() {
    Father father, *ptr_father;     
    Son son;                        // 定义第一个子类对象
    Daughter daughter;              // 定义第二个子类对象

    ptr_father = &father;           // 指向父类对象
    ptr_father->show();

    ptr_father = &son;              // 第二次指向第一个子类对象
    ptr_father->show();

    ptr_father = &daughter;         // 第三次指向第二个子类对象
    ptr_father->show();
    return 0;
}

再次运行代码,我们就会发现代码出现了报错,报错如下

抽象类不可以实例化
抽象类不可以实例化

像 Father 这样含有纯虚函数的类,就成为 抽象类,抽象类是不可以实例化的。因此,如果想要正常运行上述代码,需要将主函数中定义父类对象的代码删除。

如果必须定义一个抽象类,而且没有合适的函数作为纯虚函数,可以将析构函数定义为纯虚函数。

比如,我们可以继续拿上述代码举例,现在,我们在 Father 类中添加一个新的纯虚函数 work(),但是不对其子类 Son、Daughter 做任何改变,代码如下:

#include <iostream>
using namespace std;

// 定义父类
class Father {
public:
    virtual void show() {
        cout << "我是爸爸" << endl;
    }

    virtual void work() = 0;
};

// 定义子类
class Son: public Father {
public:
    virtual void show() {
        cout << "我是儿子" << endl;
    }
};

// 定义第二个子类
class Daughter: public Father {
public:
    virtual void show() {
        cout << "我是女儿" << endl;
    }
};

int main() {
    Father *ptr_father;     
    Son son;                        // 定义第一个子类对象
    Daughter daughter;              // 定义第二个子类对象

    ptr_father = &son;              // 指向第一个子类对象
    ptr_father->show();

    ptr_father = &daughter;         // 指向第二个子类对象
    ptr_father->show();
    return 0;
}

这时候,代码再次出现报错,报错内容却变成了因为 Son 类是抽象类,因此,不能声明 Son 对象。

这就是抽象类最后一个注意事项:抽象类派生出新的类之后,如果子类给出所有纯虚函数的函数实现,这个子类就可以定义自己的对象,因而不再是抽象类;反之,如果子类没有给出全部纯虚函数的实现,这时的子类仍然是一个抽象类。

了解了这些之后,我们就可以来看看下面这道练习题:

定义抽象动物类(Animal),定义公有的纯虚函数 speak();定义 Animal 的子类 Cat 和 Dog,其中 speak 函数分别显示猫和狗的叫声。主函数中定义 Animal 指针变量,分别指向动态生成的 Cat 和 Dog 对象,并通过指针分别调用 speak 函数,观察并分析输出结果。

代码如下:

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() = 0;
};

class Dog: public Animal {
public:
    virtual void speak() {
        cout << "汪汪汪" << endl;
    }
};

class Cat: public Animal {
public:
    virtual void speak() {
        cout << "喵喵喵" << endl;
    }
};


int main() {
    Animal* animal;
    Dog dog;
    Cat cat;

    animal = &cat;
    animal->speak();

    animal = &dog;
    animal->speak();

    return 0;
}