c++进修1:多态

众所周知,无论什么问题,只需要从三点出发,便可以快速掌握基本知识。是什么?为什么?怎么做?接下来我们开始从这三方面深入探究c++中的多态问题。

  1. 多态是啥嘞?

    顾名思义就是多种状态,细说就是不同对象完成同一件事展现出不同的状态。

    整个现在身边的栗子:不同的大三学生挑选不同的人生道路。保研同学:进实验室接触项目;就业同学:积极寻找实习;出国同学:考雅思托福。

  2. 在c++中需要什么数据结构实现上述操作呢?

    毫无疑问,我们需要一个基类Junior,;之后需要三种不同同学继承这个类,代码如下。(如果不知道什么是类,可以去看看c++的类与继承,我之后会出一期对应教程,现在可以理解为上述三种同学的共性身份)。

    class Junior {
    public:
    virtual void findRightWay() { cout << “大三了,找个出路” << endl;}
    };

    class Recommendation : public Junior {
    virtual void findRightWay() override { cout << “保研:找个导师,接触项目” << endl;}
    };

    class Employed : public Junior {
    virtual void findRightWay() override { cout << “就业:寻求实习,直接进厂” << endl;}
    };

    class Abroad : public Junior {
    virtual void findRightWay() override { cout << “出国:考雅思托福” << endl;}
    };

    void FindWay(Junior& j)
    {
    j.findRightWay();
    }

    int main()
    {
    Junior j;
    Recommendation rj;
    Employed ej;
    Abroad aj;

    FindWay(j);
    FindWay(rj);
    FindWay(ej);
    FindWay(aj);

    return 0;
    }

    多态的必须条件

    • 调用的函数必须是虚函数,而且派生类必须对基类的虚函数重写。
    • 必须通过基类指针或引用调用虚函数。

    Ps:解释一下 FindWay这里的形参必须是父类指针的引用或者指针;调用这个函数时,实参可以根据需求选择父类或者子类。

    虚函数分类:

    1. 重写: 一般的虚函数都是这种类型。派生类(子类)中有一个和基类(父类)中返回值类型相同函数名相同参数列表相同的虚函数。

    2. 协变: 派生类(子类)中有一个和基类(父类)中返回值类型不同同函数名相同参数列表相同的虚函数。(返回值类型不同仅限于:基类中的虚函数返回的是某个基类的指针或者引用,派生类中的虚函数返回的是对应的派生类中的指针或者引用)。

      class A{};
      class B : public A {};
      class Junior{
      public:
      virtual A* fun() {return new A;}
      }
      class Abroad : public Junior{
      public:
      virtual B* fun() {return new B;}
      }
      //这里不仅仅可以返回当前基类和子类的类型,还可以返回其他有继承关系的类和类型。

    Ps:析构函数必须写成虚函数。为啥子嘞?

    class Junior{
    public:
    ~Junior(){cout << “~Junior” << endl;}
    }
    class Abroad : public Junior{
    public:
    ~Abroad(){cout << “~Abroad” << endl;}
    }

    int main()
    {
    Junior* j = new Junior;
    Junior* a = new Abroad;
    delete j;
    delete a;
    return 0;
    }

    编译器将两个类的析构函数函数名都统一处理成了destructor,Junior()变为 this->destructor(),Abroad()为this->destructor() ,编译器将他们两个的函数名都统一处理成了destructor因此调用的时候只看自身的类型。现在两个的类型是 Junior,所以调用的都是~Junior(),但是我们希望a调用 ~Abroad。所以必须加上virtual,才能符合预期(需要注意:只有new出的对象指针释放的时候才需要)。

    final 和 override

    • final 可以修饰方法,也可以修饰类。修饰方法表示不能被重写,修饰类表示不能继承

    • override 和 final 相反,表示必须重写这个虚函数。没有重写会报错。

    抽象类与纯虚函数

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

  3. 其中的原理是什么?

    先看看有虚函数的基类在内存中的样子

    我在 main 函数中输出了 Junior 的大小,发现是16,但是根据普通类的大小知识,按理说应该是两个 int 类型的大小,应该是8,所以我采用了 gdb 和内存查看相结合查看缘由。

    1. 根据图中红1显示,Junior 这个类最开始是一个叫做 _vptr 的东西,我们叫它为**虚函数表(实质是一个指针)**。这个后面才是两个 int 类型。
    2. 之后我看了一下 Junior j 的地址。(红2)
    3. 在红3中,我把刚刚的地址输入,发现那个指针占8字节,剩下的两个int ,各占4字节,所以一个16字节。

    **Ps: ** 因为有了那个指针,所以这个类的对齐都变成了8字节,所以即使这个类里只有一个int,也是16字节。

    再看看重写虚函数的派生类在内存中的样子

    还是仿照上述方案,得到下图

    发现虚函数表的第一个被改掉了。