C++ 对象模型

多态(二)


多态的基础内容大家可以看我的博客”继承&&多态”:https://mp.csdn.net/mdeditor/79954623

多态的相关概念回顾

  • 虚函数:类的成员函数前加virtual关键字
  • 虚函数重写:父类和子类的虚函数如果函数名相同、参数列表相同、返回值相同(例外,协变——返回值可以不同,但是父子类中的返回值必须是继承关系的指针和引用),则成子类的虚函数重写了父类的虚函数。
  • 多态:父类的指针或者引用调用重写的虚函数,当父类的指针或者引用指向的是父类的对象则调用的是父类的虚函数,指向的是子类调用的是子类的虚函数
  • 构成多态的两个条件:1)父类的指针或者是引用;2)重写的虚函数

通过上面对多态相关概念的回顾,大家可能会有下面的疑问:如果重写的虚函数条件满足,为什么必须是父类的指针或者是引用才能构成多态?为什么不能是父类对象?

  • 我们可以先通过代码实例来看一下,如果参数p是父类的对象的时候,会发生什么?
    这里写图片描述
  • 我们可以看到,并没有构成多态,调用的都是父类的虚函数。这是为什么呢?其实,结合继承中讲到的切片/切割概念,我们不难回答这个问题。如果参数p是父类的对象,当我们将子类对象赋值给它的时候,子类对象会切出从父类中继承到的部分,所以调用的是子类的虚函数

多态的实现——虚表(虚函数表)

我的上篇博客(链接在文章开头)中在分析菱形继承问题的时候,我们有一个虚基表,它的后4个字节存放的是偏移量,前4个字节都是0,我们当时是说这是为虚表指针预留的位置,下面我们就来探索一下虚表

  • 虚表:虚函数表是通过⼀块连续内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在⼀张虚函数表,虚函数表就像⼀张地图,指明了实际应该调⽤的虚函数
  • 我们通过一个简单的实例来看一下:
    这里写图片描述
  • 结合我们之前学的虚基表,我们来整理一下这几个概念的联系:
    这里写图片描述
  • 注意:虚函数表存放的是虚函数的地址,并且以0作为结束

静态多态&&动态多态

多态,即”多种形态”,C++的多态分为静态多态和动态多态

  • 静态多态:重载,在编译期间,通过参数列表的不同去类中找这个函数,抛出这个函数的地址
  • 动态多态:通过继承重写基类的虚函数实现多态,运行时确定函数的地址

通过汇编代码来看一下静态多态和动态多态:

class Base
{
public:
    virtual void func1()
    {
        cout << "Base::func1()" << endl;
    }
    virtual void func2()
    {
        cout << "Base::func2()" << endl;
    }

    void add()
    {
        cout << "Base::add()" << endl;
    }
private:
    int a;
};

class Derive :public Base
{
public:
    virtual void func1()
    {
        cout << "Derive::func1()" << endl;
    }
    void add(int i,int j)
    {
        cout << "Derice::add(int i,int j)" << endl;
    }
};

这里写图片描述

C++对象模型


单继承模型

  • 为了演示单继承模型,我们先定义一个简单的实例:
class Base
{
public:
    virtual void func1()
    {
        cout << "Base::func1()" << endl;
    }
    virtual void func2()
    {
        cout << "Base::func2()" << endl;
    }
private:
    int _b;
};

class Derive :public Base
{
public:
    virtual void func1()
    {
        cout << "Derive::func1()" << endl;
    }

    virtual void func3()
    {
        cout << "Derive::func3()" << endl;
    }
private:
    int _d;
};


int main()
{
    Base b;
    Derive d;
}
  • 通过监视窗口观察单继承模型:
    这里写图片描述
  • 由上图我们可以看出来,子类继承了父类的虚函数表,并且重写了父类的虚函数func1(),所以子类的虚表中有重写父类的函数func1(),父类虚函数func2(),但是有一个奇怪的问题,为什么我们看不到子类的虚函数呢?按照我们的猜想子类继承了父类的虚表,所以它的虚表中有父类的虚函数,而且应该有自己的虚函数,但是我们并没有看到这个结果
  • 其实,这是编译器做的优化,子类的虚函数时存在于虚表中的,只是编译器并没有把它显示出来,这是编译器的问题,所以不要怀疑我们的猜想,下面我们继续来想办法验证
  • 既然我们无法从监视窗口看到我们想要的结果,那么我们为何不自己打印虚表来验证我们的猜想呢?毕竟,我们知道虚表指针,通过虚表指针可以找到虚表,虚表里边存放的就是虚函数的地址,并且我们知道虚表是以0结束的,这些信息足以让我们完成我们的虚表打印
  • 下面是我们虚表打印函数的实现:
typedef void(*FUNC)();
//传入虚表指针,以便找到我们的虚函数表,然后进行打印
void PrintVTable(int* vtable)
{
    cout << " 虚表地址-> " << vtable << endl;
    for (int i = 0; vtable[i] != 0; ++i)
    {
        printf(" 第%d个虚函数地址 :0x%x,-> ", i, vtable[i]);
        FUNC f = (FUNC)vtable[i];
        f();
    }
    cout << endl;
}

int main()
{
    Base b;
    Derive d;

    int* vtable1 = (int*)(*(int*)&b);
    int* vtable2 = (int*)(*(int*)&d);

    PrintVTable(vtable1);
    cout << "===========================================\n" << endl;
    PrintVTable(vtable2);
    return 0;
}
  • 我们打印虚表来与我们监视窗口来进行对应:
    这里写图片描述
  • 结果是与我们的监视窗口完全对应的,到这里我们就理解了我们的单继承模型,我们下面再画一下单继承的对象模型:
    这里写图片描述

下面我们在来补充一些小的知识点:

  • sizeof(Base)是多大呢(还是上面的类)?大部分人可能会觉得只有一个成员变量int _b,那么sizeof(Base)即为4,其实通过上面虚表的探索,我们应该否决掉这个答案,正确答案应该是8,其中有4个字节是用来存放虚表指针的(当然这是在32位平台下,如果是64位平台,一个指针的大小为8个字节,这样答案当然就是16了)
  • 我们前面说多态只跟对象有关,和类型无关,那么编译器是怎样实现的呢?编译器在程序运行起来后,只会去参数指向的那个对象的虚表中去寻找虚函数,找到哪个就调用哪个
  • 其实,虚函数是编译出来的依一段指令,不是存放在虚表中,虚表中存的是虚函数的指针,指向虚函数第一句指令的地址,表示虚函数的这段指令是存放在代码段的
  • 不同类型的对象,各自有各自的虚表(父类有父类的,子类有子类的),同类对象共用一份虚表(父类的虚函数都存在同一个虚表中)

多继承模型

  • 为了演示多继承模型,我们先定义一个简单的实例:
class Base1
{
public:
    virtual void func1()
    {
        cout << "Base1::func1()" << endl;
    }
private:
    int _b1;
};

class Base2
{
public:
    virtual void func1()
    {
    cout << "Base2::func1()" << endl;
    }
private:
    int _b2;
};

class Derive :public Base1,public Base2
{
public:
    virtual void func1()
    {
        cout << "Derive::func1()" << endl;
    }

    virtual void func2()
    {
        cout << "Derive::func2()" << endl;
    }

    virtual void func3()
    {
        cout << "Derive::func3()" << endl;
    }
private:
    int _d;
};
  • 我们通过监视窗口来初步观察一下多继承的对象模型:
    这里写图片描述
  • 通过上图我们观察到,子类继承了父类的两个虚表,并且分别重写了两个父类中的func1(),覆盖了相应虚表位置上的函数,我们同样需要和上面一样打印出虚表
  • 在打印之前,大家可以先猜测一下,既然子类有从两个父类继承过来的两个虚表,那么对于子类自己来说,它的虚函数应该放在哪一个虚表中呢?
  • 我们猜测子类的虚函数时放在Base1的虚表中的,因为子类先继承的是Base1类,下面我们打印虚表函数来验证我们的猜想,因为子类有两个虚表,第二个虚表的打印在传参数的时候需要加上偏移量:
    int* vtable1 = (int*)(*(int*)&d);
    int* vtable2 = (int*)(*(int*)((char*)&d + sizeof(Base2)));
    PrintVTable(vtable1);
    cout << "===========================================\n" << endl;
    PrintVTable(vtable2);

这里写图片描述
- 通过上图监视窗口与我们打印的虚函数表的比对,验证了我们的猜想,子类的虚函数是存放在第一个继承的父类的虚函数表中的
- 由此,我们就可以画出多继承的对象模型:
这里写图片描述

菱形继承模型

  • 为了演示多继承模型,我们先定义一个简单的实例:
class A
{
public:
    virtual void f1()
    {
        cout << "A::f1()" << endl;
    }
    virtual void f2()
    {
        cout << "A::f2()" << endl;
    }
public:
    int _a;
};

class B : public A
{
public:
    virtual void f1()
    {
        cout << "B::f1()" << endl;
    }
    virtual void f3()
    {
        cout << "B::f3()" << endl;
    }
public:
    int _b;
};

class C : public A
{
public:
    virtual void f1()
    {
        cout << "C::f1()" << endl;
    }
    virtual void f3()
    {
        cout << "C::f3()" << endl;
    }
public:
    int _c;
};

class D :public B, public C
{
public:
    virtual void f4()
    {
        cout << "D::f4()" << endl;
    }
public:
    int _d;
};
  • B、C类的f1()重写了A类的f1(),并且分别继承了A类的虚表,D类继承了B、C类的虚表,根据前面的学习,我们初步得出前面的结论,下面我们通过监视窗口来初步的观察一下菱形继承的模型:
    这里写图片描述
  • 通过内存窗口看菱形继承的模型:
    这里写图片描述
  • 通过上图我们可以看到两个地址,分别是B、C类的虚表的地址。通过前面的观察,我们不难发现,B、C类分别继承了A类的虚表,并分别重写了重写了A类的f1(),D类的虚函数时放在它第一个继承的类B的虚表中,从上面两幅图中,我们依然无法看到除了B、C类重写的A类的虚函数以及B、C类自身的虚函数
  • 同样,我们通过打印虚表来解答这个问题:
typedef void(*VFUNC)();

void PrintVTable(void* vtable)
{
    printf("vtable:0x%p\n", vtable);
    VFUNC* array = (VFUNC*)vtable;
    for (size_t i = 0; array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, array[i]);
        array[i]();
    }
    cout << "==============================================\n" << endl;
}

PrintVTable(*((int**)&d));
PrintVTable(*((int**)((char*)&d + sizeof(B))));

这里写图片描述

菱形虚拟继承模型

  • 虚拟继承我们继承部分已经讲过,注意:这里要区分两个概念,虚拟继承和虚函数虽然都有一个”虚”字并且使用的是同一个关键字virtual关键字,但它们两个并无关系
  • 虚拟继承:解决了菱形继承的数据冗余和二义性问题
  • 虚函数重写:为了实现多态

为了观察菱形虚拟继承的模型,我们定义一个和上小节完全相同的实例,在B、C类的继承方式前面加virtual关键字,我们在编译程序的时候遇到了这样的问题:
这里写图片描述

  • 因为只有一个A类,B、C类都重写了A类的f1(),编译器报错:D类的f1()继承不明确,因为不知道是从B类继承过来的还是从C类继承过来的,不明确
  • 我们如何解决这个问题呢?只能是让D类去重写f1(),我们改造一下我们刚才的实例:
class A
{
public:
    virtual void f1()
    {
        cout << "A::f1()" << endl;
    }
    virtual void f2()
    {
        cout << "A::f2()" << endl;
    }
public:
    int _a;
};

class B :virtual public A
{
public:
    virtual void f3()
    {
        cout << "B::f3()" << endl;
    }
public:
    int _b;
};

class C :virtual public A
{
public:
    virtual void f3()
    {
        cout << "C::f3()" << endl;
    }
public:
    int _c;
};

class D :public B, public C
{
public:
    virtual void f1()
    {
        cout << "D::f1()" << endl;
    }
    virtual void f4()
    {
        cout << "D::f4()" << endl;
    }
public:
    int _d;
};
  • 通过监视窗口看对象模型:
    这里写图片描述
  • 我们可以观察到B和C都继承了A的虚表,联系之前的虚继承的知识,我们可以初步猜想,这个同时存在于B和C中的A的虚表是不是放在公共的地方,毕竟在B、C中各存一份是会消耗内存的
  • 通过内存看对象模型:
    这里写图片描述
  • 我们在调试的过程中,首先看到的是5个被初始化的指针,这是由D的构造函数所做的工作,我们猜想这5个指针可能包括:B、C的虚表指针,B、C的虚基表指针,结合虚继承,还有一个可能是A的虚表指针
    这里写图片描述
  • 我们首先验证了B、C的虚基表指针,通过计算偏移量和内存中看到的结果一样。那么剩下三个指针我们猜测是B、C、A类的虚表,上图中是我们猜测的虚表中会有的函数,下面我们通过打印虚表来验证我们的猜想:
  • 打印虚表:
//B
PrintVTable(*((int**)&d));
//C
PrintVTable(*((int**)((char*)&d + sizeof(B)-sizeof(A))));
//A
PrintVTable(*((int**)((char*)&d + sizeof(B)+sizeof(C)-2*sizeof(A)+4)));
  • 结果如下图所示:
    这里写图片描述
  • 到这里,大家可能有些迷惑,既然D是继承了B、C类,那么根据我们上小节所讲的:D的虚表中应该是包含B、C类的虚表的,为什么它们单独存在呢?
  • 我们考虑一下虚继承在结合打印的虚表:我们发现B、C的虚表中并没有包含A的虚表,A的虚表被放在了最下边,这是因为A的虚表是公共的。其次D类没有自己的虚表,那么它的虚函数在哪个虚表中放呢?因为D类直接继承的是B、C类并且B类是先继承的,所以D的虚函数是放在B中的。

C++的对象模型已经总结完了,目前知识有限,只能浅薄的分析一下,后续如果有改进依然会继续更新的,如果大家发现错误或者疑问,欢迎留言!!!

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/79965411
今日推荐