C++中多态实现的原理分析、虚继承的原理

多态的原理分析

在面向对象的语言中,封装、继承、多态三大特性。我们今天说说C++中多态的实现原理。
多态往往是用来在继承中,子类中的某些行为与父类中的不同,但是为了降低调用的耦合度,我们就定义出,一个对象中有不同的形态,当然是相对与传的对象。

在C++中多态

在c++中多态就是要满足两个要求,当然前提是在继承中,因为有继承才为了满足一些要求,才出现了多态。
1)必须在继承中,存在虚函数,并且有虚函数重写 (覆盖)
2)必须是父类的指针或者引用调用重写的虚函数。
多态有什么用呢?
我们来举个例子:

#include <iostream>
#include <Windows.h>
using namespace std;

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

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

// 有个调用函数需要调用不同类中的函数。
void Fun(AA& p)
{
    p.Show();
}

int main()
{
    AA a; // 父类
    BB b; // 子类
    Fun(a);
    Fun(b);
    system("pause");
    return 0;
}

上面代码打印结果会是一样吗?
多态应用
显然不一样,可能有人就会想不应该是按照类型来进行选择吗?不是。在这里我们继承中构成了多态,所以一个Fun函数会有不同的形态,这取决于传过来的是子对象函数父对象。

那么为什么会形成这样的呢?这与多态的虚函数的存储有关 。
那么我们就来分析一下:

虚函数–虚表

虚表在单继承中
在类的成员函数中,在函数前加上关键字virtual,就形成了虚函数,虚函数在继承关系中,父类的一个成员函数声明为虚函数时,那么在继承关系的子类中,也会继承成为虚函数。

那么声明为虚函数时,如果实例化一个父类,这时候虚函数是怎么存储的?
在父类实例化后,会有一个虚函数指针,指向存虚函数的表叫虚表。怎么理解呢?

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

我们来用图了解一下:
虚表
上图中vfptr为虚表的指针指向,而下面两个为虚函数,所以就说明了虚函数是存在虚表中。

在继承中我们在看看子类继承父类后,子类也会继承父类的虚函数只不过是父类虚函数的一份拷贝并且如果子类把父类虚函数重写后,那么就会成为子类的虚函数。我们来看一下:
实验代码


#include <iostream>
#include <Windows.h>
using namespace std;

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

class BB : public AA
{
public:
    virtual void Show()
    {
        cout << "BBShow()" << endl;
    }
    virtual void Display1()
    {
        cout << "BBDisplay1()" << endl;
    }
private:
    int _b;
};

void Fun(AA& p)
{
    p.Show();
}

int main()
{
    AA a; // 父类
    BB b; // 子类
    Fun(a);
    Fun(b);
    system("pause");
    return 0;
}

运行结果测试
虚表的继承
从上图可以看出来,当子类BB重写了父类的Show()函数后,域就属于BB,没有重写的还是AA的。
我们来用画图来理解一下:
继承虚表

这样在函数传过来的对象,不管是子类对象还是父类兑现都于类型无关,传过来后,编译器会在对象头一个指针大小的内存中去找指针所指向的地址,就会去找相应的虚表,然后实行运行。这也叫动态联编
虚表在多继承中
前面讲了单继承的虚表,那么多继承虚函数虚表就很好理解,是建立在单继承的基础之上的。我们用图来解释一下。
多继承虚表
多继承中,如果父类中都有虚函数,那么有几个父类就会有几个虚函数。

虚继承

为什么会有虚继承这个概念呢?
虚继承是为了解决多继承带来的问题(菱形继承),多继承中如果一个A类,分别被C,B类继承,C,B又被D类继承,那么D中就会有两份A中的成员,一份来自C,一份来自B,这样就会产生两个问题,二义性问题,数据冗余问题。
为了解决这个问题,在C++中出现了虚继承的概念,为了解决这两个问题。那么虚继承到底是怎么解决这个问题的呢?
继承就是子类继承父类的所有成员,那么子类一个子类的大小就是子类大小加上父类的大小。但是在虚拟继承中就有所差别,我们先来在vs上运行观察一下结果:


#include <iostream>
#include <Windows.h>
using namespace std;

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

class BB : virtual public AA
{
public:
    void Show()
    {
        cout << "BBShow()" << endl;
    }
    void Display1()
    {
        cout << "BBDisplay1()" << endl;
    }
private:
    int _b;
};

class CC : virtual public AA
{
public:
    void Show()
    {
        cout << "BBShow()" << endl;
    }
    void Display1()
    {
        cout << "BBDisplay1()" << endl;
    }
private:
    int _c;
};

class DD : public BB, public CC
{
public:
    void Show()
    {
        cout << "BBShow()" << endl;
    }
    void Display1()
    {
        cout << "BBDisplay1()" << endl;
    }
private:
    int _d;
};
int main()
{
    cout<< sizeof(AA) << endl;
    cout << sizeof(BB) << endl;
    cout << sizeof(CC) << endl;
    cout << sizeof(DD) << endl;
    system("pause");
    return 0;
}

运行结果
我们在AA类中定义了int 的_a,所以我们这是在32为环境下运行,是四个字节。
那么BB类继承AA后加上自身的int _b总共应该是8个字节,那么为什么会是12个字节呢?
在这里虚继承就是会在子类中多开辟出四个字节,其实是一个指针类型,指向的是一个表叫虚基表。虚基表是干什么用的我们一会说。我们在看看CC类也是12个字节,那么它也是在类中多存储了一个指针。

我们看DD类,是24个字节,我们来计算一下,BB 和 CC被继承,那么加自身的应该是28,为什么是24,在BB,CC虚继承后,那么再DD继承中,会继承一份_a但是会多出两个指针。怎么理解呢?
DD对象
这是直观上是这样的。在vs下指向的是一个虚基表
vs
我们看一下内存
我们为了看起来方便,给_a初始化成10,其他没有初始化
内存
在同中的0x0088dc8c就是BB类中指向虚基表的指针,而虚表中存的是当前指针开是的偏移量。从而找到_a.

在学习过程中,有一个问题补充一下!
为什么在继承中最好把析构函数定义成虚函数?
经过我的验证和多方资料查询。得出结论,在一种情况下,当子类对象赋给父类指针或者引用的时候,把析构函数修饰为虚函数时候,就会先调用子类的析构函数,后调用父类的析构函数,这个时候就可以完全的析构完。如果不修饰为虚函数,就只会调用父类的析构函数,不会调用子类的析构函数,这样如果是堆上的空间就会内存泄漏!

class A
{
public:
    virtual ~A()
    {
        cout<<"~A()"<<endl;
    }   
};

class B : public A
{
public:
    ~B()
    {   
        cout<<"~B()"<<endl;
    }   
};

void fun(A* q)
{
    q->~A();
}

int main()
{
    A* a = new B;
    fun(a);
    return 0;
}

验证结果

如果有误,还望指正,谢谢!

猜你喜欢

转载自blog.csdn.net/gangstudyit/article/details/80295239