多态实现的机制

什么是多态?

  • 多态是C++编程时的一种特性,多态性即是对一个接口的多种实现。多态可以分为静多态和动多态。所谓静多态就好比函数重载、模板,静多态是在函数编译阶段就决定调用机制,即在编译连接阶段就将函数的入口地址给出。而动多态是在程序运行的时候才决定调用机制。下面我们主要来讨论动多态。

首先来了解一些多态的基本知识:

  • 在类中用virtual关键字声明的函数叫做虚函数。
  • 存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
  • 多态性是一个接口多种实现,是面向对象的核心。
  • 多态用虚函数来实现,结合动态绑定。
  • 纯虚函数是虚函数再加上= 0。例如virtual void Show() = 0;
  • 抽象类是指包括至少一个纯虚函数的类。

多态实现机制:

#include<iostream>
using namespace std;

class Object
{
    public:
    virtual void fun(int x = 10)
    {
        cout << "print Object x= " << x << endl;
    }
};

class Test: public Object
{
    private:
    void fun(int y = 20)
    {
        cout << "print Test y = " << y << endl;
    }
};

int main()
{
    Test t1;
    Object *p = &t1;
    p->fun(); 
    return 0;
}

//输出结果为:print Test y = 10
#include<iostream>
using namespace std;

class Object
{
    public:
    void fun(int x = 10)
    {
        cout << "print Object x= " << x << endl;
    }
};

class Test: public Object
{
    private:
    void fun(int y = 20)
    {
        cout << "print Test y = " << y << endl;
    }
};

int main()
{
    Test t1;
    Object *p = &t1;
    p->fun(); 
    return 0;
}
//当我们把virtual关键字去掉的时候程序的输出结果为:print Object x = 10
  • 通过上面的的代码和两种不同的形式下的两种结果我们能够知道当有virtual关键字修饰函数的时候函数是呈现动多态的。p是基类类型的指针,现在指向了派生类,但是在派生类中有两部分,即派生类自己的部分和继承基类的部分,此时的p是指向派生类中的基类部分的。所以程序在编译期间程序拿到的是基类中fun函数的形参x = 10,但在运行期间在运行期间由于virtual关键字的存在形成了动多态,即动态绑定,在派生类中对基类的fun函数进行了隐藏(如果要调用Object中的fun()的加上作用域,即p->Object::fun() ),但是此时拿到的参数任然为在基类中拿到的10,所以打印的结果为:print Test y = 10

  • 在不加virtual关键字的情况下,我们构造Test类的对象时,首先要调用Object类的构造函数去构造Object类的对象,然后才调用Test类的构造函数完成自身部分的构造,从而拼接出一个完整的Object对象。当我们将Test类的对象转换为Object类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是下图中的“animal的对象所占内存”。那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法,由于p是基类Object的指针,所以他调用的是基类中的fun()函数。
    这里写图片描述

在virtual关键字的背后发生了什么?

  • 编译器在编译的时候,发现类中有(virtual修饰的函数)虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即vtable,一个类的所有对象共享一个虚表),该表是一个一维数组,在这个数组中存放每个虚函数的地址。对上面程序,Object和Test类都包含了一个虚函数fun(),因此编译器会为这两个类都建立一个虚表,(即使基类里面没有virtual函数,但是其派生类里面有,所以子类中也有了)。
  • 那么如何定位虚表呢?编译器另外还为每个类的对象提供了一个虚表指针(即vptr,一个对象有一个),这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。对于上述程序,由于p实际指向的对象类型是Test,因此vptr指向的Text类的vtable,当调用p->fun()时,根据虚表中的函数地址找到的就是Test类的fun()函数。而在虚表中存的有虚函数指针(即vfptr,每个虚函数有一个),虚函数指针就指向相应的虚函数。
  • 正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。那么虚表指针在什么时候,或者说在什么地方初始化呢?
    • 答案是在构造函数中进行虚表的创建和虚表指针的初始化。在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。对于程序来说,当Test类的对象构造完毕后,其内部的虚表指针也就被初始化为指向Test类的虚表。在类型转换后,调用p->fun(),由于p实际指向的是Test类的对象,该对象内部的虚表指针指向的是Test类的虚表,因此最终调用的是Test类的fun()函数。
    • 要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。

类的多态和函数的多态

  • 类的多态性是指用虚函数和延迟绑定来实现的。函数的多态性是函数的重载和模板的早绑定来实现的。
  • 一般情况下(没有涉及virtual函数),当我们用一个指针/引用调用一个函数的时候,被调用的函数是取决于这个指针/引用的类型。即如果这个指针/引用是基类对象的指针/引用就调用基类的方法;如果指针/引用是派生类对象的指针/引用就调用派生类的方法,当然如果派生类中没有此方法,就会向上到基类里面去寻找相应的方法。这些调用在编译阶段就确定了。
  • 当设计到多态性的时候,采用了虚函数和动态绑定,此时的调用就不会在编译时候确定而是在运行时确定。不在单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数。

补充

  • 模板也是静多态的早绑定是因为模板生成代码的代码,模板特例化出函数之后不同的参数类型就形成了函数的重载,而函数的重载就是早绑定的静多态。因此模板为静多态的实质就是函数重载为静多态。
发布了62 篇原创文章 · 获赞 68 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/magic_world_wow/article/details/81588747