[C++面向对象语言第三大特性] :多态
目录
- 多态的概念
- 多态的构成条件
- 虚函数
- 重写
- 区别重写和重载
- 构成多态后的效果
- 有关于继承和多态的关键字
- 特殊的类 :纯虚类(抽象类)
多态的概念
多态按字面的意思就是多种形态。
继承是多态的基础
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态允许使用基类指针或引用来调用子类的重写方法,从而使得同一接口可以表现不同的行为。
多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。
多态的构成条件
1.必须由指针或者引用调用虚函数
2.基类和派生类的虚函数必须构成重写(所以继承是多态的条件)
虚函数
在基类中声明一个函数为虚函数,使用关键字virtual。
派生类可以重写(override)这个虚函数。
调用虚函数时,会根据对象的实际类型来决定调用哪个版本的函数。
重写
重写的概念非常简单,即派生类有一个,返回值类型,函数名,参数列表与基类完全相同的相同函数
注意点1 : 重写只是对函数体的重写(即只针对函数的实现不同)
注意点2 :特别的析构函数的重写
基类和派生类的析构函数一定是不同名的啊,怎么构成重写?答:编译器会统一把析构函数名处理成 deseructor,就是为了让他构成多态
这里p2是开了student空间的大小,但是却是person类型,这里又不构成多态
所以P2调基类的析构函数导致student有些部分没有被析构到,所以内存泄漏
使用virtual关键字修饰析构函数构成重写后,就可以进行多态析构了
#include <iostream>
using namespace std;class Person {
public:// 基类析构函数定义为「虚函数」virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:// 派生类析构函数(自动成为虚函数,与基类虚析构构成「重写」)virtual ~Student() { cout << "~Student()" << endl; }
};int main() {Person* p1 = new Person;delete p1; // 调用 Person 的虚析构函数Person* p2 = new Student;delete p2; // 因析构函数是虚函数,会先调用 Student::~Student(),再调用 Person::~Person()return 0;
}
这就是C++核心指南中有这么一条原则
C.35: A base class destructor should be either public and virtual, or protected and non-virtual
区别重写和重载
重载通常在同一作用域下,ex:一个类中,一个命名空间中,一个全局作用域中
而重写是不同作用域下的(最常见的就是基类和派生类中)
构成多态后的效果
1.构成多态:
#include <iostream>
using namespace std;// 基类 Person
class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};// 派生类 Student(公有继承 Person)
class Student : public Person
{
public:// 重写(override)基类的虚函数 BuyTicketvirtual void BuyTicket(){cout << "买票半价" << endl;}
};// 接收 Person 引用的函数,会触发多态行为
void Func(Person& people)
{people.BuyTicket();
}// 测试函数
void Test()
{Person Mike; // 定义 Person 类型对象 MikeFunc(Mike); // 调用 Func,传入 Mike(调用 Person::BuyTicket)Student Johnson; // 定义 Student 类型对象 JohnsonFunc(Johnson); // 调用 Func,传入 Johnson(调用 Student::BuyTicket,多态)
}int main()
{Test();return 0;
}
完成了构成图多态的两个条件,Mike的引用和Johnson引用将调用不同的虚函数,构成了多态指针或引用的指向与创建时指向的类型有关
2.不构成多态
#include <iostream>
using namespace std;// 基类:动物
class Animal {
public:// 普通成员函数(非虚函数)void Speak() {cout << "动物发出声音" << endl;}// 普通析构函数(非虚函数)~Animal() {cout << "Animal析构函数被调用" << endl;}
};// 派生类:猫(继承自动物)
class Cat : public Animal {
public:// 与基类同名的普通成员函数(非虚函数,不构成重写)void Speak() {cout << "猫喵喵叫" << endl;}// 派生类普通析构函数(非虚函数)~Cat() {cout << "Cat析构函数被调用" << endl;}
};int main() {// 基类指针指向基类对象Animal* a1 = new Animal;a1->Speak(); // 调用基类的Speak(根据指针类型)delete a1; // 调用基类的析构函数// 基类指针指向派生类对象(不构成多态)Animal* a2 = new Cat;a2->Speak(); // 仍然调用基类的Speak(因无虚函数,不根据对象实际类型)delete a2; // 仅调用基类的析构函数(派生类析构未被调用,可能导致资源泄漏)return 0;
}
因为缺少虚函数的重写所以不构成多态,对象调用的类型与其创建时指向的类型无关,之和其本身的类型有关,在上述代码片中
本身的类型就是“Animal”
有关于继承和多态的关键字
final:表示最终
修饰虚函数,该虚函数无法被重写
修饰类,该类成为最终类,无法被继承
override : 修饰虚函数,该虚函数无法被重写
可以检测出一些很“恶心”的bug,比如下图防止check名字写错写成chcek了
特殊的类 :纯虚类(抽象类)
包含纯虚函数的类叫纯虚类(抽象类),纯虚类无法实例化生成对象
纯虚函数:在虚函数后面 加上 =0 就是纯虚函数了
纯虚类作为基类的作用:
纯虚类一定包含了纯虚函数,它规定了派生类必须重写这个纯虚函数否则派生类无法生成实例
因此纯虚类的作用就是规范,派生类必须对某个纯虚函数进行重写
另外纯虚函数也体现了接口继承
现在我们已经完成面向对象语言三大特性,封装,继承,多态基本的学习了,后续的博客会讲解,多态和虚继承的原理(即虚函数指针表和虚基表)