多态的概念

多态按字面的意思就是多种形态。
继承多态的基础
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

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)基类的虚函数 BuyTicket
    virtual void BuyTicket()
    {
        cout << "买票半价" << endl;
    }
};

// 接收 Person 引用的函数,会触发多态行为
void Func(Person& people)
{
    people.BuyTicket();
}

// 测试函数
void Test()
{
    Person Mike;       // 定义 Person 类型对象 Mike
    Func(Mike);        // 调用 Func,传入 Mike(调用 Person::BuyTicket)

    Student Johnson;   // 定义 Student 类型对象 Johnson
    Func(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 就是纯虚函数了

纯虚类作为基类的作用:
纯虚类一定包含了纯虚函数,它规定了派生类必须重写这个纯虚函数否则派生类无法生成实例
因此纯虚类的作用就是规范,派生类必须对某个纯虚函数进行重写
另外纯虚函数也体现了接口继承

在这里插入图片描述
在这里插入图片描述
现在我们已经完成面向对象语言三大特性,封装,继承,多态基本的学习了,后续的博客会讲解,多态和虚继承的原理(即虚函数指针表和虚基表)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐