C++之多态的实现以及虚表

在C++中,多态:见名知意,就是多种形态.虚函数的主要作用就是实现多态,就是父类的指针/引用调用重写的虚函数,当父类的指针/引用指向父类对象时调用的是父类的虚函数,指向子类时指向子类的虚函数.

多态的实现原理

在有虚函数的对象实例中都存在一张虚函数表(简称虚表),而虚表中存放的则是实际应该调用的虚函数.
下面我们通过代码来模拟查找一下:

class AA
{
public:
    virtual void Show()
    {
        cout<<"Show()"<<endl;
    }
    virtual void Display()
    {
        cout<<"Display()"<<endl;
    }
private:
    int _a;
};

在VS的环境中,在一个类中写两个虚函数,是怎么存储的呢?
这里写图片描述
通过求类的大小和从监视窗口中可以看到,类的大小指的是成员变量的大小而不包含成员函数.在存储虚函数的时候是存一个虚函数指针,然后将每个虚函数的地址,每个地址指向对应的虚函数.所以在该类中虚函数的大小为8,是因为类中有一个成员变量和一个指向虚函数表的指针组成的.

单继承与虚表

class AA
{
public:
    virtual void Show()
    {
        cout<<"Show()"<<endl;
    }
    virtual void Display()
    {
        cout<<"Display()"<<endl;
    }
private:
    int _a;
};
class BB:public AA
{
public:
    virtual void Fun()
    {
        cout<<"Fun()"<<endl;
    }
private:
    int _b;
};

BB类继承了AA类,并且有自己的虚函数.
这里写图片描述
同样的从监事窗口查看类的大小和虚函数的存储,类的大小为12(因为继承了AA类的同时,还有一个BB类自己的成员变量以及一个虚函数表指针),可是虚函数表中却没有BB类中自己的独有的虚函数???其实呢,这儿就是VS的一个BUG所在,虚函数Fun()就在Display()虚函数的下面.
遇到上面的情况呢,我们可以通过打印虚函数表来查看:

typedef void (*show)();
void ShowVtrPtr(int** table)
{
    cout<<"打印虚表:"<<table<<endl;
    int i = 0;
    for(;table[i] != NULL;++i)//注意:虚表结束的标志为NULL
    {
        cout<<" "<<i<<" "<<table[i]<<"  ";
        show s = (show)table[i];
        s();
    }
    cout<<endl;
}

同时打印上面两个类AA与BB.打印结果如下图所示.
这里写图片描述
我们发现,如果类中有虚函数,就一定至少有一个有一个指向所有虚函数的指针.简单的理解一下,当子类继承父类时,也会将对应的虚函数继承下来,继承虚函数的过程为:

  1. 先开辟空间.
  2. 拷贝父类的虚表.
    3.如果在子类中重写了父类中的函数,此时就会在集成后将父类的虚表覆盖并将其改为子类的虚表.
    可以发现,当类定义好以后,就不会再随意去修改类,所以虚表一般会存在于内存中的常量区或者静态区.

多继承与虚表

当一个类有两个父类或多个父类时,又会发生什么故事呢?

class AA
{
public:
    virtual void Show()
    {
        cout<<"AA::Show()"<<endl;
    }
    virtual void Display()
    {
        cout<<"AA::Display()"<<endl;
    }
private:
    int _a;
};
class BB
{
public:
    virtual void Show()
    {
        cout<<"BB::Show()"<<endl;
    }
    virtual void Display()
    {
        cout<<"BB::Display()"<<endl;
    }
    virtual void BB1()
    {
        cout<<"BB::BB1()"<<endl;
    }
private:
    int _b;
};
class CC:public AA,public BB
{
private:
    int _c;
};

CC类同时继承了AA类和BB类.
这里写图片描述

    CC c;
    cout<<sizeof(c)<<endl;  

此时,sizeof(c)的大小为20,却不是16,说明此时CC类在继承了AA类和BB类时,不止生成了一个虚表指针,而是有两个虚表指针.
可以通过打印虚表指针来查看:

    ShowVtrTable((int**)(*(int**)&c));   //**二级指针可以解决任何平台下的虚表打印 
    ShowVtrTable((int**)(*(int**)((char*)&c + sizeof(a))));

这里写图片描述
即在内存中是这样存储的:(先继承的在前面,所以如果CC类有自己的虚函数时,也会将虚函数表存于先继承的那个表里(在本类中即存于AA类的虚表中))
这里写图片描述

菱形继承与虚表

我们都知道:菱形继承会存在两个问题:①数据冗余–>造成浪费内存.②二义性(即不确定性).解决:->虚继承(会产生自己的虚基表).
那么,当菱形继承中遇到了虚函数,遇到了虚表那又会发生什么样的故事呢?

class AA
{
public:
    virtual void Show()
    {
        cout<<"AA::Show()"<<endl;
    }
    virtual void Display()
    {
        cout<<"AA::Display()"<<endl;
    }
    int _a;
};
class BB:virtual public AA
{
public:
    virtual void Show()
    {
        cout<<"BB::Show()"<<endl;
    }
    virtual void Display()
    {
        cout<<"BB::Display()"<<endl;
    }
    virtual void BB1()
    {
        cout<<"BB::BB1()"<<endl;
    }
    int _b;
};
class CC:virtual public AA
{
public:
    virtual void Show()
    {
        cout<<"CC::Show()"<<endl;
    }
    virtual void CC1()
    {
        cout<<"CC::CC1()"<<endl;
    }
    int _c;
};
class DD:public BB,public CC
{
public:
    virtual void Show()
    {
        cout<<"DD::Show()"<<endl;
    }
    int _d;
};

BB和CC分别虚继承了AA类.然后DD类同时继承了BB类和CC类.
此时,DD类的大小为:36.我们可以通过查看内存:

    DD d;
    d.BB::_a = 1;
    d.CC::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    cout<<sizeof(d)<<endl;  //--->36

这里写图片描述
总结:所以加起来DD类的大小就是36,并且在DD类中有3个虚表,分别是AA类,BB类和CC类的虚表,同时,DD类的虚表指针从与BB类的虚表指针指向的是同一个地方,但是它们只是指针相同,含义是不同的.
那么,哪个是虚表指针,而哪个又是虚函数表指针呢?很简单啊,我们可以再打开一个内存的监事窗口来查看.
这里写图片描述
总结:在VS的环境下,先存的是虚函数表指针,再存储的是虚基表指针.
然后我们可以打印出这个例子中的3个虚函数表:

    ShowVtrTable((int**)(*(int**)&d));
    ShowVtrTable((int**)(*(int**)((char*)&d + sizeof(b) - sizeof(a))));
    ShowVtrTable((int**)(*(int**)((char*)&d + sizeof(d) - sizeof(a))));
    system("pause");

这里写图片描述

多态(与对象有关)

即多种状态.在C++ 中,多态分为静态多态和动态多态.

  • 静态多态:即重载.由于是在编译期间确定的,所以就是静态多态.
  • 动态多态:通过继承重写基类

构成多态的条件:

  1. 父类的指针或引用.
  2. 调用的函数必须是虚函数的重写.

写写一个简单的多态来观察:

class AA
{
public:
    virtual void Show()
    {
        cout<<"AA::Show()"<<endl;
    }
private:
    int _a;
};
class BB:virtual public AA
{
public:
    virtual void Show()
    {
        cout<<"BB::Show()"<<endl;
    }
private:
    int _b;
};
class CC:virtual public AA
{
public:
    virtual void Show()
    {
        cout<<"CC::Show()"<<endl;
    }
private:
    int _c;
};
class DD:public BB,public CC
{
public:
    virtual void Show()
    {
        cout<<"DD::Show()"<<endl;
    }
private:
    int _d;
};
void f(AA& A)    //父类的指针或引用
{
    A.Show();
}

可以通过不同的对象来调用同一个函数.

    AA a;
    BB b;
    CC c;
    DD d;
    f(a);
    f(b);
    f(c);
    f(d);

那么,为什么可以这么使用呢?我们可以通过反汇编来查看它们.
这里写图片描述

总结:静态多态在编译期间就可以找到函数对应的地址,从而去掉用它.
动态多态是在运行期间才去找调用的函数.

这里写图片描述

猜你喜欢

转载自blog.csdn.net/yinghuhu333333/article/details/80227585
今日推荐