多态
- 多态的概念
- 多态的定义和实现
- 重写
- 抽象类
- 多态的原理
- 虚表的构建原理
- 虚函数的调用原理
多态的概念
多态就是多种形态,传递不同的对象,会调用不同的方法。
多态的定义和实现
那么在C++语法中,多态是如何实现的呢?
我们首先要在继承体系下,要构成多态,需要满足以下两个条件:
1.通过基类的指针或引用来调用虚函数
2.被调用的函数必须是虚函数,并且派生类要对基类的虚函数进行重写。
我们可能又有一个问题了,什么是虚函数?
实际上在类中,被virtual修饰的函数就是虚函数
class A
{
public:
virtual void fun()
{
cout << "virtual func()";
}
};
重写
由于实现多态需要对基类中的虚函数进行重写,我们就要明白什么是虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
换句话说,子类中的函数除了实现方式不同,其他都和基类的虚函数完全一样,这样就实现了虚函数的重写。而这样的方式,实际上子类中重写的函数也是虚函数,我们建议为了更加清晰的看到当前子类的这个函数是虚函数,建议在函数返回值前用virtual修饰。(修饰不修饰,它都是虚函数)
而有两种额外情况下,即使重写的虚函数返回值不同或者名称不同,依然构成虚函数的重写:
协变和析构函数
协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person
{
public:
virtual A* f() {return new A;}
};
class Student : public Person
{
public:
virtual B* f() {return new B;}
};
析构函数:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person
{
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,
// 下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
由于虚函数也是可以继承的,当我们将子类中重写了基类的虚函数也继承下来,也就意味着还能在孙类中继续重写,从而实现多态,那么我们如果不想让孙类再重写子类中的虚函数,就可以给子类中重写的虚函数末尾加上final
进行修饰
而当前子类某些函数是否已经重写了基类的虚函数,需要进行判断,就可以使用override
进行判断,加到要判断的虚函数末尾,如果报错,则说明没有进行重写
class Car
{
public:
virtual void Drive(){}
};
class Benz :public Car
{
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
抽象类
抽象类的概念:在虚函数的后面写上
=0
,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
换句话说:如果存在纯虚函数,那么这个类就是抽象类,只能被继承,不能被实例化,并且只有继承后子类将基类中所有的纯虚函数重写后,子类才可以进行实例化。
多态的原理
一个类中,如果定义了虚函数,那么在对象模型中就存在一个虚表,这个虚表实际上就是一个函数指针数组,数组的每个元素都是虚函数的地址。
注:在同一个类中,不同对象共用同一个虚表,并且这个虚表是在编译阶段生成的。
class A
{
public:
virtual void fun1(){cout << "A::fun1()" << endl;}
virtual void fun2(){cout << "A::fun2()" << endl;}
virtual void fun3(){cout << "A::fun3()" << endl;}
int _a;
};
上图就是A类的对象模型,由于fun1,fun2,fun3都是虚函数,则编译器会在编译阶段将三个虚函数的地址放入A类的虚表中,然后给对象模型创建一个虚表指针。
虚表的构建原理
基类虚表的构建原理:在编译阶段,将虚函数按照类中声明顺序依次添加到虚表中。
子类虚表的构建原理:针对单继承
1.将基类虚表中的内容拷贝一份到子类虚表中
2.如果子类重写了基类中某个虚函数,则使用子类的虚函数地址替换虚表中相同位置的基类虚函数地址
3.将子类新增加的虚函数按照在类中声明的先后次序依次添加到虚表的最后。
虚函数的调用原理
对于虚函数的调用原理:
我们首先要在传递参数时,传递基类的引用或指针。
如果代码在运行阶段传递的基类对象,那么调用顺序就是:
1.通过基类对象来获取基类的虚表地址。
2.传递this指针
3.通过传递的this指针来获取虚函数的地址
4.调用对应的虚函数
如果传递的是子类对象,也是类似的调用原理:
1.通过子类对象来获取子类的虚表地址。
2.传递this指针
3.通过传递的this指针来获取虚函数的地址
4.调用对应的虚函数