多态

1.概念

通俗来说,多态就是函数调用的多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的动作和结果。在继承关系中想要构成多态需要满足两个条件:

  • 必须通过基类的指针或者引用调用虚函数;(如果直接使用对象本身,那么多态将无法实现,因为在编译时,编译器会根据对象的静态类型即声明时的类型来决定
    调用那个方法,而不是根据对象的动态类型)。
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行了重写。

2.虚函数的重写

虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同),此时就称该派生类的虚函数重写了基类的虚函数。在重写基类的虚函数的时候,派生类的虚函数不加virtual也可以构成重写,主要是因为继承后基类的虚函数被继承了下来,在派生类中依旧保持虚函数属性。

虚函数重写的两个例外:

  • 协变(基类和派生类虚函数的返回值类型不同)
    派生类重写基类的虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称之为协变。
#include <iostream>
using namespace std;

// 基类
class Animal {
public:
    virtual Animal* clone() const {
        return new Animal(*this);
    }
    virtual void makeSound() const {
        cout << "Some generic animal sound" << endl;
    }
};

// 派生类
class Dog : public Animal {
public:
    // 重写 clone 方法,返回类型是 Dog*
    Dog* clone() const override {
        return new Dog(*this);
    }
    void makeSound() const override {
        cout << "Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    // 重写 clone 方法,返回类型是 Cat*
    Cat* clone() const override {
        return new Cat(*this);
    }
    void makeSound() const override {
        cout << "Meow!" << endl;
    }
};

void demonstrateCovariance(const Animal& animal) {
    Animal* clonedAnimal = animal.clone();
    clonedAnimal->makeSound();
    delete clonedAnimal;
}

int main() {
    Dog dog;
    Cat cat;
	// 这里也构成了多态。
    demonstrateCovariance(dog); // 输出 "Woof!"
    demonstrateCovariance(cat); // 输出 "Meow!"
    return 0;
}
  • 析构函数的重写
    如果基类的析构函数为虚函数,此时派生类中的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数的名字不一致。但是在程序编译后析构函数的名字统一都会被处理成destructor()。
int main()
{
	//分别new一个父类对象和子类对象,并均用父类指针指向它们
	Person* p1 = new Person;
	Person* p2 = new Student;

	//使用delete调用析构函数并释放对象空间
	delete p1;
	delete p2;
	return 0;
}

考虑上述的场景,若是父类和子类的析构函数没有构成重写就可能会导致内存泄露,因此此时delete p1和delete p2都是调用的父类的析构函数,而这里期望的是p1调用父类的析构函数,p2调用子类的析构函数,即一种多态的行为。这里只有将父类和子类的析构函数都定义成虚函数构成重写,才能使得delete按照我们的预期进行析构函数的调用,才能实现多态。

关键字override和final

  • final:修饰虚函数,表示该虚函数不能被重写
  • override:检查派生类的虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。

3.抽象类

在虚函数的后面加上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

抽象类存在的意义:

  • 可以更好的去表示现实世界,没有实例对象对应的抽象类型
  • 抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为如果子类不去重写纯虚函数那么子类将也是一个抽象类,无法实例化出对象。

4.多态的原理

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

可以计算出来,Base类实例化出现的对象的大小为8个字节。除了成员变量_b外,还有一个_vfptr放在对象的前面,该指针称之为虚函数表指针,也成为虚表指针,每一个包含虚函数的类中都至少有一个虚表指针。虚表指针指向一个虚函数表,简称虚表,虚表当中存储的就是虚函数的地址,对于Base的对象,其_vfptr指向
的就是虚函数Func1的地址(Base::Func1),如果Device类继承了Base类,同时重写了虚函数Func1,那么Device对象指向的虚表中存储的就是重写后的Func1的地址(Device::Func1)。这也是为什么虚函数的重写也叫做覆盖,覆盖就是指表中虚函数地址的覆盖,重写是语法上的叫法,覆盖是原理层的叫法。

这里注意:虚函数表本质是一个存储虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

派生类的虚函数表生成步骤:
1.先将基类的虚表内容拷贝一份到派生类的虚表中
2.如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址
3.派生类自己新增加的虚函数地址按其在派生类中的声明次序增加到派生类虚表的最后

虚表在什么阶段初始化的?虚函数存放在哪里?虚表存在哪里?
虚表实际上是构造函数初始化列表阶段进行初始化的,虚表中存储的是虚函数的地址,虚函数都是存储在代码段的。同时虚表是存储在代码段上面的。

动态绑定和静态绑定:
静态绑定:又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,例如函数重载
动态绑定:又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态绑定。

在单继承关系中,派生类的虚表生成的过程:
1.继承基类的虚表内容到派生类的虚表
2.对派生类重写的虚函数地址进行覆盖
3.虚表当中新增派生类当中新的虚函数地址

在多继承关系中,派生类的虚表生成过程:
1.分别继承各个基类的虚表内容到派生类的各个虚表中
2.对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖)
3.在派生类第一个继承基类部分的虚表中新增派生类当中新的虚函数地址

posted @ 2024-09-12 22:41  alone_qing  阅读(27)  评论(0)    收藏  举报