C++中的多态:虚函数
上一章讲解了C++中类的继承,这一节将讲解面向对象编程的核心:多态
本章内容如下:
- 什么是多态
- 虚函数的用途和用法
- 什么是抽象类以及如何声明他们
- 什么是虚继承以及在什么情况下使用虚继承
多态基础
多态的英语是ploymorphism, ploy源自于希腊语,是多的意思,而morhph是形态的意思。
多态是面向对象语言的基础,能够让我们以类似的方式处理不同而又类似的对象。
多态即多种形态,指的是在面向对象语言中,接口的多种不同的实现方式。其目的是为了实现借口重用,对于函数而言就是对函数名等标识符进行重复利用
之所以称为多态是因为同一个函数名的函数具有多种形态,能够根据不同的输入来进行不同的调用。
所以实际上我们前面所讲的,对函数进行重载和使用重载构造函数就是多态的一个具体的例子,本节的主要讲解C++类中的多态
类中的多态也是相同的含义,我们将在下面进行讲解
类中的多态
类中的多态引入
我们首先看下面的一个例子
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim(){
cout << "金枪鱼在游泳" << endl;
}
};
void FishSwim(Fish & aFish){
aFish.Swim();
}
int main(){
Tuna aTuna;
FishSwim(aTuna);
return 0;
}
编译成功之后运行得到如下的结果
jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryPolymorphism
>>>
鱼在游泳
我们期望得到的结果是调用Tuna类中的Swim方法,但是因为我们FishSwim函数中声明接受一个Fish对象的引用,因此我们在FishSwim中调用Swim方法实际上会运行Fish类中的Swim方法
我们想要解决这个问题,一种思路就是对FishSwim这个函数进行重载,编写一个接受Tuna类的引用的FishSwim函数。但是本章的内容针对的是类中的多态,因此我们将使用另外一种方法来解决这个问题,就是通过将Fish类中的Swim方法声明为虚函数从而解决这个问题
使用虚函数
我们使用关键字virtual来定义一个虚函数
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim(){
cout << "金枪鱼在游泳" << endl;
}
};
void FishSwim(Fish & aFish){
aFish.Swim();
}
int main(){
Tuna aTuna;
FishSwim(aTuna);
return 0;
}
编译成功后运行得到如下结果
jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryPolymorphism
>>>
金枪鱼在游泳
这里我们使用了关键字virtual来将Fish类中的Swim方法定义为了一个虚函数,这样就解决了这个问题
类中的多态的含义
这里我们不禁要问了,我们在FishSwim函数中命名传入的是一个Fish类的引用,我们调用这个引用的各种方法按理来说应该表现出Fish类的方法,但是这里在上面的例子中却表现出了另外一个类Tuna的特性,即基类表现出了派生类的性态
这就是类中的多态的含义,让基类表现派生类的性态,从而使得基类具有了多种形态
我们实现类中的多态,具体的方法就是使用virtual关键字来定义虚函数。
虚函数
上面讲解了类中的多态的含义,我们发现实现类的多态的核心就是使用关键字virtual来将类中的方法声明为虚函数,进而实现类中的多态
而virtual关键字的本质是降低对应函数在调用时的优先级,优先让派生类的函数发挥作用
下面将详细讲解C++中的虚函数
使用关键字virtual定义虚函数
我们可以使用关键字virtual来定义一个虚函数
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim(){
cout << "金枪鱼在游泳" << endl;
}
};
void FishSwim(Fish & aFish){
aFish.Swim();
}
int main(){
Tuna aTuna;
FishSwim(aTuna);
return 0;
}
定义虚构造函数
我们在前面讲过,我们实例化一个派生类对象的时候,首先会调用基类的构造函数来为派生类的实例化做好准备,接下来才会调用派生类的构造函数
那么结合上面讲的多态,就会有一个问题了,我们如果让指向基类的指针表现出派生类的特性的话,那么将会出现什么样的结果?
我们以下面的例子为例进行讲解
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
Fish(){
cout << "调用Fish类的构造函数" << endl;
}
~Fish(){
cout << "调用Fish类的析构函数" << endl;
}
};
class Tuna : public Fish
{
public:
Tuna(){
cout << "调用Tuna类的构造函数" << endl;
}
~Tuna(){
cout << "调用Tuna类的析构函数" << endl;
}
};
void DeletePoniter(Fish * aFishPointer){
cout << "进入DeletePoniter函数内部" << endl;
cout << "开始释放指向Tuna的指针" << endl;
delete aFishPointer;
}
int main(){
cout << "创建指向Tuna的指针" << endl;
Tuna * aTunaPointer = new Tuna;
DeletePoniter(aTunaPointer);
return 0;
}
这里我们在DeletePointer函数中声明的是指向Fish类的指针,虽然我们传入的是指向Tuna类的指针,从而使得函数中指向基类的指针具有了派生类的特性。我们编译成功之后运行得到如下结果
jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryPolymorphism
>>>
创建指向Tuna的指针
调用Fish类的构造函数
调用Tuna类的构造函数
进入DeletePoniter函数内部
开始释放指向Tuna的指针
调用Fish类的析构函数
我们发现具有多态的指针在释放的时候并不会调用派生类的析构函数,这将会导致资源未释放,内存泄露等问题
为此,我们需要降低基类中的析构函数的优先级,将其声明为虚函数即可
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
Fish(){
cout << "调用Fish类的构造函数" << endl;
}
virtual ~Fish(){
cout << "调用Fish类的析构函数" << endl;
}
};
class Tuna : public Fish
{
public:
Tuna(){
cout << "调用Tuna类的构造函数" << endl;
}
~Tuna(){
cout << "调用Tuna类的析构函数" << endl;
}
};
void DeletePoniter(Fish * aFishPointer){
cout << "进入DeletePoniter函数内部" << endl;
cout << "开始释放指向Tuna的指针" << endl;
delete aFishPointer;
}
int main(){
cout << "创建指向Tuna的指针" << endl;
Tuna * aTunaPointer = new Tuna;
DeletePoniter(aTunaPointer);
return 0;
}
编译成功之后运行得到的结果如下
jackwang@jackwang-ThinkPad-X390-Yoga:~/桌面/TryC++$ ./TryPolymorphism
>>>
创建指向Tuna的指针
调用Fish类的构造函数
调用Tuna类的构造函数
进入DeletePoniter函数内部
开始释放指向Tuna的指针
调用Tuna类的析构函数
调用Fish类的析构函数
这样就能避免上面的问题
虚函数的工作原理:虚函数表
前面的代码中我们在FishSwim函数中声明的是接受一个Fish的引用,接下来调用Fish中的Swim方法,但是为函数传入一个Tuna类的对象完全是由用户所决定的,编译器事先是无法预知我们到底会向函数中传入什么类型的参数
如果我们向其中放入一个Fish类,函数调用了Fish类的Swim方法,这是完全正常的。但是我们向其中放入一个Tuna类,编译器也能够知道调用Tuna类的Swim方法(在使用虚函数的情况下)。
其实这是虚函数的工作原理所导致的,下面就将讲解虚函数的工作原理:虚函数表
我们将基于下面的代码进行讲解
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void FunctionFish_1(){}
virtual void FunctionFish_2(){}
virtual void FunctionFish_3(){}
};
class Tuna : public Fish
{
public:
virtual void FunctionTuna_1(){}
virtual void FunctionTuna_2(){}
};
int main(){
return 0;
}
上面的代码中,我们在Fish类中声明了三个虚函数,分别是:Function_1,Function_2,Function_3
在Tuna类中,我们声明了两个将会覆盖Fish类的虚函数:Function_1和Function_2
编译器在编译的时候,当发现派生类中覆盖了基类中的虚函数时候,就会为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(Virtual Function Table,VFT)
虚函数表本质上是一个静态的指针数组,其中的指针分别指向各自类的虚函数,如果基类中具有派生类没有的虚函数,那么派生类的VFT中将会包含指向基类的虚函数的指针
所以上面的例子中,两个类的虚函数表如下
[*FunctionFish_1,*FunctionFish_2,*FunctionFish_3]
[*FunctionTuna_1,*FunctionTuna_2,*FunctionFish_2]
此外,当我们实例化一个Fish类或者Tuna类的时候,编译器都会创建一个隐藏的指针(称为*VFT)指向对应类型的VFT
因此我们使用下面语句的时候,编译器会查找Tuna类中的虚函数表,来确保调用FunctionFish_2
Tuna aTuna;
aTuna.FunctionFish_2();
同样,当我们以下面的方式来调用,也会在Tuna类的虚函数表中查找
void FishSwim(Fish & aFish){
aFish.FunctionTuna_1();
}
int main(){
Tuna aTuna;
FishSwim(aTuna);
return 0;
}
虽然我们将aTuna对象传入了FishSwim函数导致aTuna被视为一个Fish实例来进行处理,但是其VFT指针依旧指向Tuna类的虚函数表,因此会执行Tuna的FunctiionTuna_1.
我们通过下面的代码来确定的确创建了虚函数表
#include <iostream>
//导入命名空间
using namespace std;
class FishWithoutVirtual
{
public:
void FunctionFish_1(){}
void FunctionFish_2(){}
void FunctionFish_3(){}
};
class FishWithVirtual
{
public:
virtual void FunctionFish_1(){}
virtual void FunctionFish_2(){}
virtual void FunctionFish_3(){}
};
int main(){
cout << "类FishWithoutVirtual的大小是:" << sizeof(FishWithoutVirtual) << endl;
cout << "类FishWithVirtual的大小是:" << sizeof(FishWithVirtual) << endl;
return 0;
}
编译成功之后运行得到的结果如下
jackwang@jwang:~/桌面/TryC++$ ./TryPloymorphism
>>>
类FishWithoutVirtual的大小是:1
类FishWithVirtual的大小是:8
抽象基类和纯虚函数
不能实例化的基类成为抽象基类,抽象基类只有一个用途:从抽象基类中派生其他类
而我们创建抽象基类的方法就是声明其中的函数为纯虚函数
而且作为抽象基类的派生类中必须覆盖纯虚函数,即定义有效的与纯虚函数同名的函数
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim() = 0;
};
int main(){
return 0;
}
这样我们声明的Fish类就是一个抽象基类,而其中的Swim()就是纯虚函数
我们创建一个继承Fish类的Tuna类
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim() = 0;
};
class Tuna : public Fish
{
public:
void Swim(){
cout << "金枪鱼有用" << endl;
}
};
int main(){
return 0;
}
所以抽象基类的功能就是约束派生类,即在抽象基类中统一规定所有继承自该抽象基类的派派生类必须具有的方法
虚继承解决多继承问题
在讲解多继承的时候,我们使用了两栖动物的例子,两栖动物派生自陆生动物和水生动物,而陆生动物和水生动物都是派生自动物的,所以两栖动物存在菱形继承的问题
示例代码如下
#include <iostream>
//导入命名空间
using namespace std;
class Animal
{
public:
int age;
Animal(){
cout << "执行了Animal的构造函数" << endl;
}
};
class AquaticAnimal : public Animal
{
public:
AquaticAnimal(){
cout << "执行了AquaticAnimal的构造函数" << endl;
}
};
class TerrestrialAnimal: public Animal
{
public:
TerrestrialAnimal(){
cout << "执行了TerrestrialAnimal中的构造函数" << endl;
}
};
class Amphibia : public AquaticAnimal, public TerrestrialAnimal
{
public:
Amphibia(){
cout << "执行了Amphibia中的构造函数" << endl;
}
};
int main(){
Amphibia aAmphibia;
return 0;
}
这样我们在实例化Amphibia对象的时候,会首先实例化AquaticAnimal和TerrestrialAnimal来做准备,进而首先会执行两次Animal来做准备
因此,编译成功之后运行得到的结果如下
jackwang@jwang:~/桌面/TryC++$ ./TryPloymorphism
>>>
执行了Animal的构造函数
执行了AquaticAnimal的构造函数
执行了Animal的构造函数
执行了TerrestrialAnimal中的构造函数
执行了Amphibia中的构造函数
而这种方式会导致两个问题
首先是我们实例化一个Amphibia对象会自动创建两个Animal的实例,这就造成了内存的浪费.
其次,由于Animal中还要age属性,所以Amphibia类中实际上具有两个age属性,分别是:AquaticAnimal::Animal::age
和TerrestrialAnimal::Animal::age
所以我们单纯的使用aAmphibia.age
的时候会报错,因为编译器不知道具体要调用那个属性
更加可笑的是,我们可以分别为这两个属性设置值,即便都是aAmphibia对象的属性
解决这个问题,我们可以使用虚继承来解决
使用关键字virtual实现虚继承
我们在声明可能被作为基类继承的派生类的继承声明时候使用virtual关键字,这样确保在实例化时候只会对Animal进行一次实例化
#include <iostream>
//导入命名空间
using namespace std;
class Animal
{
public:
int age;
Animal(){
cout << "执行了Animal的构造函数" << endl;
}
};
class AquaticAnimal : public virtual Animal
{
public:
AquaticAnimal(){
cout << "执行了AquaticAnimal的构造函数" << endl;
}
};
class TerrestrialAnimal: public virtual Animal
{
public:
TerrestrialAnimal(){
cout << "执行了TerrestrialAnimal中的构造函数" << endl;
}
};
class Amphibia : public AquaticAnimal, public TerrestrialAnimal
{
public:
Amphibia(){
cout << "执行了Amphibia中的构造函数" << endl;
}
};
int main(){
Amphibia aAmphibia;
return 0;
}
编译之后运行得到如下结果
jackwang@jwang:~/桌面/TryC++$ ./TryPloymorphism
>>>
执行了Animal的构造函数
执行了AquaticAnimal的构造函数
执行了TerrestrialAnimal中的构造函数
执行了Amphibia中的构造函数
这个时候就可以使用语句aAmphibia.age
来对age属性进行操作
使用关键字override来表明覆盖意图
前面我们在派生类中覆盖基类的方法都是通过相同特征值来进行覆盖的
而有的时候我们如果不小心笔误将函数的特征值写错了,那么就不会造成覆盖,例如
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim() const {
cout << "金枪鱼在游泳" << endl;
}
};
这里我们不小心使用了关键字const,这样虽然对程序的运行没有大碍,但是由于Tuna中的Swim具有特征值const,因此和Fish中的特征值不同
所以实际上不会造成覆盖.
为了解决这种问题,我们可以使用关键字override来声明覆盖意图,这样编译器将在编译时候进行检查,如果没有可被覆盖的值将会报错,例如下面的程序
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim() const override
{
cout << "金枪鱼在游泳" << endl;
}
};
int main(){
return 0;
}
编译报错如下
jackwang@jwang:~/桌面/TryC++$ g++ TryPloymorphism.cpp -o TryPloymorphism
>>>
TryPloymorphism.cpp:17:10: error: ‘void Tuna::Swim() const’ marked ‘override’, but does not override
void Swim() const override
^~~~
而基类中具有可以被覆盖的函数时候就不会报错
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim() override
{
cout << "金枪鱼在游泳" << endl;
}
};
int main(){
return 0;
}
编译成功通过
使用关键字final禁止覆盖函数
我们可以在类中使用关键字final来禁止函数在其派生类中再次被覆盖
#include <iostream>
//导入命名空间
using namespace std;
class Fish
{
public:
virtual void Swim(){
cout << "鱼在游泳" << endl;
}
};
class Tuna : public Fish
{
public:
void Swim() override final
{
cout << "金枪鱼在游泳" << endl;
}
};
class DeadTuna : public Tuna
{
public:
void Swim(){
cout << "死亡的金枪鱼在游泳" << endl;
}
};
int main(){
return 0;
}
我们编译的时候会报错,报错信息如下
jackwang@jwang:~/桌面/TryC++$ g++ TryPloymorphism.cpp -o TryPloymorphism
>>>
TryPloymorphism.cpp:26:10: error: virtual function ‘virtual void DeadTuna::Swim()’
void Swim(){
^~~~
TryPloymorphism.cpp:17:10: error: overriding final function ‘virtual void Tuna::Swim()’
void Swim() override final
^~~~
不能将复制构造函数声明为虚函数
我们使用关键字virtual来声明虚函数的时候,表明它将被派生类中的函数覆盖,这种多态行为是在运行阶段实现的
但是由于复制构造函数只是用于创建固定类型的对象,因此不具备多态性
所以C++中不允许将复制构造函数声明为虚函数