【c++学习笔记】多态

多态到底是什么呢?

字面意思就是同种事物在不同的场景下所表现出不同的形态。
在c++当中,多态分类如下:
这里写图片描述

在学习多态之前,我们必须得先了解虚函数的概念。

  • 虚函数就是在类的成员函数(除构造函数、拷贝构造函数、静态成员函数)前加virtual关键字。
class B
{
public:
    virtual void TestFunc()
    {
        cout << "Base::TestFunc()" << endl;
    }
    int _b;
};

int main()
{
    B b;
    cout << sizeof(B) << endl;

    return 0;
}

这里写图片描述
这里打印结果为什么是8不是4呢?
这里写图片描述
在内存窗口上&B之后,发现它的前4个字节放的类似地址的东西,那么这个地址指向的又是什么呢?
这里写图片描述
将前四个字节的类似地址的东西放到内存窗口上查看后,发现这里放的还是地址,那么这里的地址是什么呢?
其实这里的地址就是虚函数的地址,在带有虚函数的类中,会多开辟四个字节用来存放指向一张虚表的指针,虚表里放的都是虚函数的地址。要注意的是,虚函数只有在继承体系中才有意义,因为在非继承体系用不到,还多开辟了4字节的空间。带有虚函数的类的对象模型如下:
这里写图片描述
静态多态在这里不过多介绍,主要学习动态多态

动态多态的条件:

  • 基类中必须包含虚函数,并且派生类一定要对基类中的虚函数进行重写
    • 重写:
    • 要和基类中的虚函数原型相同(返回值、参数列表、函数名均相同)(协变和虚拟析构除外)
    • 协变:返回值可以不同,但是基类的虚函数必须返回基类对象的指针(引用);派生类的虚函数必须返回派生类对象的指针(引用)–这里不符合返回值相同,但是也是重写。
    • 析构函数:析构函数也可以作为虚函数,并且在继承体系中建议作为虚函数(为什么建议稍后解释)
  • 通过基类的指针(引用)调用虚函数。

多态的含义:

  • 如果基类的指针/引用指向/引用基类的对象,那么在调用虚函数时调用属于基类的虚函数。
  • 如果基类的指针/引用指向/引用派生类的对象(赋值兼容规则),那么在调用虚函数时调用属于派生类的虚函数。
class B
{
public:
    virtual void TestFunc()//加virtual关键字,成员函数将作为虚函数
    {
        cout << "Base::TestFunc()" << endl;
    }

};

class D : public B
{
public:
    virtual void TestFunc()//派生类中对虚函数进行重写,函数名、返回值、参数列表必须一致,
                           //在重写时,派生类中的访问限定符不会对虚函数有什么影响,且可以不加virtual关键字
    {
        cout << "Derived::TestFunc()" << endl;
    }
};

void Test(B& b)//通过基类的引用调用虚函数
{
    b.TestFunc();
}

int main()
{
    D d;
    B b;

    Test(d);
    Test(b);

    return 0;
}

那么,单继承中派生类的对象模型是怎样的呢?

带有虚函数单继承对象模型

class B
{
public:
    virtual void TestFunc()//加virtual关键字,成员函数将作为虚函数
    {
        cout << "Base::TestFunc()" << endl;
    }

    int _b;
};

class D : public B
{
public:
    virtual void TestFunc()
    {
        cout << "Derived::TestFunc()" << endl;
    }

    int _d;
};

int main()
{
    D d;
    d._b = 1;
    d._d = 2;

    cout << sizeof(d) << endl;

    return 0;
}

这里写图片描述
在调试的时候调出内存窗口,&d可以很容易得出带有虚函数的单继承对象模型:
这里写图片描述

普通成员函数和虚函数的区别:
最大的区别就是调用方式不同,普通成员函数直接调用,而虚函数的调用分为如下几步:

  • 从对象前4个字节中取虚表的地址
  • 传递this指针
  • 从虚表中获取虚函数的地址(虚表地址+虚函数在虚表中的偏移量)
  • 调用虚函数

带有虚函数的多继承对象模型

class B1
{
public:
    virtual void TestFunc1()
    {
        cout << "Base1::TestFunc1()" << endl;
    }

    int _b1;
};

class B2
{
public:
    virtual void TestFunc2()
    {
        cout << "Base2::TestFunc2()" << endl;
    }

    int _b2;
};

class D: public B1, public B2
{
public:
    virtual void TestFunc1()//对B1中的虚函数TestFunc1重写
    {
        cout << "Derived::TestFunc1()" << endl;
    }

    virtual void TestFunc2()
    {
        cout << "Derived::TestFunc2()" << endl;//对B2中的虚函数TestFunc2重写
    }

    virtual void TestFunc3()//派生类自己特有的虚函数
    {
        cout << "Derived::TestFunc3()" << endl;
    }


    int _d;
};


int main()
{
    D d;
    d._b1 = 1;
    d._b2 = 2;
    d._d = 3;

    cout << sizeof(D) << endl;

    return 0;
}

这里写图片描述
对&d得:
这里写图片描述
发现这里有两个类似地址得东西,查看得
这里写图片描述
这里写图片描述
不难发现,这两个地址分别指向两张虚表,一张为继承B1的、另一张为继承B2的,第一张虚表还会存放派生类中特有的虚函数。所以,对象模型如下:
这里写图片描述

带有虚函数的菱形继承

#include <iostream>
using namespace std;

class B
{
public:
    virtual void TestFunc1()
    {
        cout << "B::TestFunc1()" << endl;
    }
    virtual void TestFunc2()
    {
        cout << "B::TestFunc2()" << endl;
    }

    virtual void TestFunc3()
    {
        cout << "B::TestFunc3()" << endl;
    }

    int _b;
};

class C1 : public B
{
public:
    virtual void TestFunc1()
    {
        cout << "C1::TestFunc1()" << endl;
    }
    int _c1;
};

class C2 : public B
{
public:
    virtual void TestFunc2()
    {
        cout << "B::TestFunc2()" << endl;
    }

    int _c2;
};

class D : public C1, public C2
{
public:
    virtual void TestFunc1()
    {
        cout << "D::TestFunc()" << endl;
    }

    int _d;
};

int main()
{
    D d;
    cout << sizeof(D) << endl;

    return 0;
}

这里写图片描述

对象模型:
其实从派生类D的大小很容易就能推断出对象的模型如下:
这里写图片描述
由于菱形继承存在数据二意性的问题,所以就引出了带有虚函数的菱形虚拟继承

带有虚函数的菱形虚拟继承

首先,先看看在单继承中,带有虚函数的虚拟继承的对象模型,这里是为了方便理解带有虚函数的菱形虚拟继承的对象模型,因为在单继承中,虚拟继承是没有什么实际意义的。

class B
{
public:
    virtual void TestFunc()
    {
        cout << "B::TestFunc()" << endl;
    }

    int _b;
};

class D : virtual public B
{
public:
    virtual void TestFunc()
    {
        cout << "D::TestFunc()" << endl;
    }

    int _d;
};

int main()
{
    D d;

    cout << sizeof(D) << endl;
    d._b = 1;
    d._d = 2;

    return 0;
}

这里写图片描述
在对象d中,除了有一张拷贝B的虚表外,因为是虚拟继承,还有一张保存偏移量的表格,调出内存窗口,取地址,如下:
这里写图片描述
发现,确实是有两个指针,那么,哪一个是指向虚表的,哪一个又是指向保存偏移量的呢,我们可以再调用一个内存窗口进行查看,因为虚表中保存的是地址,而另一个保存的是偏移量,是整数。
这里写图片描述
这里写图片描述
可以发现,上面的地址指向存放偏移量的表格,下面的指向虚表,所以再单继承中,带有虚函数的虚拟继承对象模型如下:
这里写图片描述
上面的是派生类没有新增自己特有的虚函数的模型,接下来我们用同样的方法看看在派生类中新增虚函数后,对象模型又是什么?

class B
{
public:
    virtual void TestFunc1()
    {
        cout << "B::TestFunc1()" << endl;
    }

    int _b;
};

class D : virtual public B
{
public:
    virtual void TestFunc1()
    {
        cout << "D::TestFunc1()" << endl;
    }
    virtual void TestFunc2()
    {
        cout << "D::TestFunc2()" << endl;
    }

    int _d;
};

int main()
{
    D d;

    cout << sizeof(D) << endl;
    d._b = 1;
    d._d = 2;

    return 0;
}

这里写图片描述
算下来大小比上面的多了4字节。其实就是如果派生类自己新增虚函数,就会多开辟四个字节,指向另一张虚表,这张虚表中存放派生类自己特有的虚函数地址。
这里写图片描述

接下来,再来看看带有虚函数的菱形虚拟继承,这里为了简单起见,没有在派生类中新增特有的虚函数。

class B
{
public:
    virtual void TestFunc1()
    {
        cout << "B::TestFunc1()" << endl;
    }

    virtual void TestFunc2()
    {
        cout << "B::TestFunc2()" << endl;
    }

    virtual void TestFunc3()
    {
        cout << "B::TestFunc3()" << endl;
    }

    int _b;
};

class C1 : virtual public B
{
public:
    virtual void TestFunc1()
    {
        cout << "C1::TestFunc1()" << endl;
    }
    int _c1;
};

class C2 : virtual public B
{
public:
    virtual void TestFunc2()
    {
        cout << "C2::TestFunc2()" << endl;
    }
    int _c2;
};

class D : public C1, public C2
{
public:
    virtual void TestFunc3()
    {
        cout << "D::TestFunc3()" << endl;
    }

    int _d;
};


int main()
{
    D d;

    cout << sizeof(C1) << endl;
    cout << sizeof(D) << endl;
    d._b = 1;
    d._c1 = 2;
    d._c2 = 3;
    d._d = 4;

    return 0;
}

这里写图片描述
对象模型如下:
这里写图片描述
讲到这里,现在我们来想想动态多态的实现原理

动态多态实现原理

  • 编译器在带有虚函数的类的背后维护了一张虚表(虚函数的入口地址)
  • 虚函数的调用原理(通过基类的指针/引用调用虚函数)
    • 从指针所指对象(基类/派生类)前4个字节中取虚表的地址
    • 传递参数(this+当前虚函数的参数)
    • 根据从对象4个字节取到的虚表的地址取对象的虚函数地址
    • 调用虚函数

现在我们再来谈谈为什么静态函数,构造函数,拷贝构造函数为什么不能作为虚函数?
- 最主要的原因就是调用虚函数需要this指针,但是这几个函数当中并没有虚函数(或者还没构造好对象)
那么为什么建议将析构函数作为虚拟函数呢?
答案是因为如果不作为虚拟函数,有可能会造成内存泄露的问题,如下:

class B
{
public:
    B()
        :_b(1)
    {
        cout << "B::B()" << endl;
    }

    ~B()
    {
        cout << "B::~B()" << endl;
    }
    int _b;
};

class D: public B
{
public:
    D()
        :_ptr(new char[10])
    {
        cout << "D::D()" << endl;
    }

    ~D()
    {
        if (_ptr)
        {
            delete[] _ptr;
        }

        cout << "D::~D()" << endl;
    }

    char* _ptr;
};

void TestFunc()
{
    B* pb;

    pb = new D;//由于赋值兼容规则,基类指针可以指向派生类对象,这里就会调用D的构造函数申请空间
    delete pb;//由于是基类类型的指针,所以delete只会调用基类的析构,所以这里就会造成内存泄漏
}

int main()
{
    TestFunc();

    return 0;
}

这里写图片描述
只要我们将析构函数作为虚函数,就不会出现上面的问题了,打印结果如下:
这里写图片描述
那么,为什么不直接将析构函数默认作为虚函数呢?

  • C++不 把虚析构函数直接作为默认值的原因是虚函数表的开销以及和C语言的类型的兼容性。

猜你喜欢

转载自blog.csdn.net/virgofarm/article/details/80896937
今日推荐