多态

虚函,协变,重写

定义

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为

分类:

  • (静态绑定)静态多态:不同的参数,调用不同的函数。——本质是重载
  • (动态绑定)动态多态:通过继承中的虚函数重写 + 父类指针或者引用对齐进行调用,并且根据不同对象调用不同的函数。本质是虚函数表。

条件

  • 必须通过基类的指针(切片)或者引用调用虚函数。
  • 调用的函数必须是虚函数,且派生类必须对基类的虚函数进行——重写

总结:多态 = 基类的(指针或者引用)调用虚函数 + 重写

多态举例

虚函数

注意:

  • 成员函数才可以使用virtual

  • 派生类可以不加virtual,而父类则不行,因为子类重写时,会在virtual的基础上进行重写。 ——但建议都加上。

  • 由于虚函数要进入虚表从而不能内联inline,就算加上也会被编译器忽略。
    类中定义的函数默认会成为内联

虚函数的重写

条件:派生类虚函数与基类虚函数的以下条件完全相同,则称称子类的虚函数重写了基类的虚函数 —— 即构成多态
条件:

  • 返回值类型
  • 函数名字
  • 参数类型和参数个数(注意:和形参无关,也和缺省值无关)——故虚函数继承也被称为接口继承

协变

破坏了以上条件中返回值类型相同的条件后,依然维持多态的一种方式。

1
2
3
4
5
6
7
8
9
10
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;}
};

允许返回值类型不同实现的多态,但要求返回值必须是父子关系的引用或者指针

  • 父子关系的引用或者指针(父子都需要是引用,或者都是指针)
    • 父类返回父类型的引用或者指针。
    • 子类返回子类型的引用或者指针。

多态原理

虚函数是通过虚函数表(vtable)和虚函数指针(vptr)来实现的:

  • 虚函数表(vtable):

    • 每个包含虚函数的类都有一个虚函数表,虚函数表是一个函数指针数组,数组中的每个指针指向该类的虚函数的实现。
  • 虚函数指针(vptr)

    • 每个对象都有一个虚函数指针,指向类的虚函数表。当通过基类指针或引用调用虚函数时,程序会通过对象的虚函数指针找到对应的虚函数表,并从表中调用正确的函数。

虚函数表

用于记录虚函数(__vfptr)的地址(位于代码段)

  • 对于一个不符合多态的普通调用,会在编译阶段,(根据调用者的类型,再更具函数名修饰规则),找到要调用函数的地址。

  • 符合多态,那就在运行时,到所指向对象的虚函数表当中找到调用函数的地址

    • 如果是父类,那么就去父类的虚函数表中找。
    • 如果是子类,那么就去子类的虚函数表中找,子类会将继承的虚表用自己的虚表——覆盖
  • 并且,在一个子类赋值给父类的时候,即对象切片发生的时候,不会拷贝虚表。否则会导致使用父类的对象却使用子类的方法。

虚函数表

纯虚函数

虚函数的后面写上 =0 ,则这个函数为纯虚函数
例如:

1
2
3
public:
virtual void Drive() = 0;/*只用声明,不用有实现*/
}

抽象类

  • 包含纯虚函数的类叫做抽象类(也叫接口类)
  • 抽象类不能实例化出对象。派生类继承后也不能实例化出对象。
  • 只有重写纯虚函数,派生类才能实例化出对象。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

final

final:修饰虚函数,或者基类(c++11,使其无法被继承),表示该虚函数不能再被重写

override

override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错

经典题目

  1. 求下述程序的运行结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class A
    {
    public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
    };

    class B : public A
    {
    public:
    void func(int va=0){ std::cout<<"B->"<< va <<std::endl; }
    };

    int main(int argc ,char* argv[])
    {
    B*p = new B;
    p->test();
    return 0;
    }

分析

  1. 由于函数是保存到代码段的,所以派生类不会额外生成函数,因此在调用的时候,也是由其所属的类直接调用。
    • test()位于基类,故调用的时候,也是基类的指针调用。
  2. 并且满足虚函数重写的条件,故满足多态,因此,回去调用派生类中的func,故有B->
  3. 由于重写是在基类的基础上,重写的是实现,其参数列表的值都来自于基类,派生类参数列表只是为了检查是否满足多态条件 **(参数类型,参数个数相同,和形参无关) **,故结果是B->1

多态
https://weihehe.top/2024/07/05/多态/
作者
weihehe
发布于
2024年7月5日
许可协议