一、什么是多态?
在面向对象编程中,我们通常将多态分为两种类型:静态多态(静态多态,也称为编译时多态)和动态多态(动态多态性,也称为运行时多态)。这两种多态性是多态概念的不同表现方式。
1、静态多态
(1)静态多态是指在编译时就能确定要调用的方法,通过函数重载和运算符重载来实现。
(2)函数重载是指在一个类中定义多个同名函数,但是参数类型或个数或前后顺序不同,编译器根据调用时传入的参数类型和数量来确定调用那个函数。
(3)运算符重载是指重新定义运算符的行为,以使其适用于自定义类。编译器在编译阶段就会确定调用那个运算符的重载版本。
2、动态多态
(1)动态多态是指在运行时根据对象的实际类型来确定要调用的函数,通过继承和函数覆盖来实现。
(2)继承允许派生类继承基类的函数,并且派生类可以重写基类的函数以实现自己的行为。
(3)当使用基类的指针或引用指向派生类对象时,通过虚函数机制,程序在运行时会调用对应派生的函数。
静态多态性发生在编译时,因为在编译阶段编译器就可以确定要调用的函数;而动态多态性发生在运行时,因为具体调用那个函数是在程序运行时根据对象的实际类型确定的。
注:本文中后续说的多态均为动态多态。
二、多态的概念
多态可以理解为“一种接口,多种状态”,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。
多态的使用具有三个前提条件:
(1). 公有继承
(2). 函数覆盖
(3). 基类引用/指针指向派生类对象
多态的优点:多态的优势包括代码的灵活性、可扩展性和可维护性。它能够使代码更具通用性,减少重复代码的编写,并且能够轻松地添加新的派生类或扩展现有的功能。
多态的缺点:多态的缺点包括代码的复杂性、运行效率、易读性。当类的继承关系复杂时,理解和维护多态性相关的代码会变得困难。动态多态在运行中会产生一些额外的开销,因为需要在运行时确定对象的实际类型并调用相应的函数。这种开销通常比静态多态调用要高。过度使用多态性可能会导致代码不易理解。
三、多态的实现
3.1 函数覆盖
函数覆盖、函数隐藏。这两个比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必备条件。函数覆盖比函数隐藏有以下几点区别:
(1). 函数隐藏是派生类中存在与基类中同名同参的函数,编译器会将基类的同名同参数的函数进行隐藏。注:基类中的函数得是非虚函数的普通函数。
(2). 函数覆盖是基类中定义了一个虚函数,派生类编写一个同名同参数的函数将基类中的虚函数进行重写并覆盖。注:覆盖的基类函数必须是虚函数。
3.2 虚函数的定义
一个函数使用virtual关键字修饰,就是虚函数,虚函数是函数覆盖的前提,在Qt Creator中虚函数的函数名称使用斜体字。
例如: virtual void eat();
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物吃东西"<< endl;
}
};
虚函数具有以下性质:
(1). 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。例如:
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物吃东西"<< endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的vritual可写可不写
void eat()
{
cout << "狗吃骨头"<< endl;
}
};
(2). 只有普通成员函数与析构函数可以声明为虚函数。
例如:
class Animal
{
public:
// 错误 构造函数不能声明为虚函数
// virtual Animal()
// {
// cout << "测试:构造函数虚函数" << endl;
// }
// 错误 静态函数不能为虚函数
// virtual static void testStatic()
// {
// cout << "测试:静态成员函数虚函数" << endl;
// }
// 虚函数
virtual void eat()
{
cout << "动物吃东西"<< endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的vritual可写可不写
void eat()
{
cout << "狗吃骨头"<< endl;
}
};
(3). 在C++11中,可以在派生类的新覆盖的函数上使用override关键字验证覆盖是否成功。
例如:
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物吃东西"<< endl;
}
void funHide()
{
cout << "测试:override关键字函数"<< endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的vritual可写可不写
void eat() override
{
cout << "狗吃骨头"<< endl;
}
// 错误 标记覆盖但是没覆盖。
// 注:这是函数隐藏,并不是函数覆盖因为基类中的同名函数不是虚函数
// void funHide() override
// {
// cout << "测试:override关键字函数"<< endl;
// }
};
3.3 多态实现
我们在开篇时提到过,要实现动态多态,需要有三个必要前提条件:
(1). 公有继承 (上述代码已经实现)
(2). 函数覆盖 (上述代码已经实现)
(3). 基类引用/指针指向派生类对象(还未编写)
【思考】为什么要基类引用/指针指向派生类对象?
(1). 实现运行时多态:通过将基类的指针指向派生类对象,可以实现运行时多态。当使用基类的指针或引用来指向派生类对象时,程序在运行时会根据对象的实际类型来调用相应的函数,而不是根据指针或引用的类型。
(2)统一接口:基类的指针可以作为一个通用的接口,用于操作不同类型的派生类对象。这样可以使代码更灵活,减少重复的代码,并且支持代码的扩展和维护。
代码如下:
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物吃东西"<< endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗爱吃骨头"<< endl;
}
};
int main()
{
// 基类指针指向派生类对象
Animal *a1 = new Dog;
// 调用派生类覆盖的虚函数
a1->eat(); // 狗爱吃骨头
// 基类的引用指向派生类对象
Dog d1;
Animal &a2 = d1;
// 调用派生类覆盖的虚函数
a2.eat(); // 狗爱吃骨头
return 0;
}
我们也可以提供通用接口,参数设计为基类的指针或引用,这样这个函数就可以访问到此基类所有派生类中的虚函数了。代码如下:
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物吃东西"<< endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗吃骨头"<< endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "猫吃鱼" << endl;
}
};
// 提供通用函数,形参为基类引用
void animal_eat1(Animal &al)
{
al.eat();
}
// 提供通用函数,形参为基类指针
void animal_eat2(Animal *al)
{
al->eat();
}
int main()
{
// 传递引用
Animal a1;
Dog d1;
Cat c1;
animal_eat1(a1); //动物吃东西
animal_eat1(d1); //狗吃骨头
animal_eat1(c1); //猫吃鱼
// 传递指针
Animal *a2 = new Animal;
Dog *d2 = new Dog;
Cat *c2 = new Cat;
animal_eat2(a2); //动物吃东西
animal_eat2(d2); //狗吃骨头
animal_eat2(c2); //猫吃鱼
return 0;
}
四、多态的原理
具有虚函数的类会存在一张虚函数表,这张表被当前类所有对象共用,每个类的对象内部会有一个隐藏的虚函数表指针成员,指向当前类的虚函数表。
多态实现流程:
在代码运行时,通过对象的虚函数表指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应的虚函数内容。