多态&多态对象模型

来源:互联网 发布:网络与新媒体就业方向 编辑:程序博客网 时间:2024/06/03 09:25

(动态)多态:当使用基类的指针或引用调用重写的虚函数,当指向父类调用的就是父类的虚函数,指向子类调用的就是子类的虚函数。
不难剖析出要实现多态:
首先要有虚函数的重写,其次要用基类的指针去调用。

那么先简单认识下虚函数&虚函数的重写:
虚函数:类的成员函数加了 virtual 关键字。
虚函数的重写:当在子类中定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的虚函数。

程序一
看下面的代码

#include<iostream>using namespace std;class person{public:    virtual void Buyticket()    {          cout << "成人-全价" << endl;    }};class student :public person{    virtual void Buyticket()    {        cout << "学生-半票" << endl;    }};void func(person& p){    p.Buyticket();}int main(){    person p;    student s;    func(p);    func(s);    system("pause");    return 0;}

不难分析出运行结果是:
成人-全价
学生-半票

这个程序实现了一个简单的多态,显然它符合构成多态的两个条件:
虚函数的重写基类的指针或引用

思考
派生类的指针或引用否可行?
当然是不可行的,子类的指针或引用赋给父类时会发生切片行为,父类指向的部分是父类与子类共有的。如果是子类的指针或引用,就有可能出现越界访问。

重载(静态多态),重定义(隐藏),重写(覆盖)各自的定义?
这里写图片描述
上图列出了,这三个的基本构成条件。要分别认识他们,就需要思考它们的出现实现了什么功能,解决了什么问题。
重载:在同一作用域内函数名相同,参数的不同(类型或个数),根据传的参数来实现不同的功能,例如类里面的构造函数与拷贝构造函数。
重定义:⼦类和⽗类中有同名成员,⼦类成员将屏蔽⽗类对成员的直接访问。相当于从父类继承下来的成员不隐藏掉,要想访问需要加域作用符去访问。
重写:是为了实现多态引入的,子类调用指向子类的虚函数,父类调用指向父类的虚函数。

简单的总结下多态:
C++中虚函数的主要作⽤就是实现多态。简单说⽗类的指针/引⽤调⽤重写的虚函数,当⽗类指针/引⽤指向⽗类对象时调⽤的是⽗类的虚函数,指向⼦类对象时调⽤的是⼦类的虚函数。

构成多态类型决定了你调用的方法,没有构成多态对象决定了调用的方法。
不理解的可以看下面的程序

程序二

#include<iostream>using namespace std;class person{public:    virtual void Buyticket()    {          cout << "成人-全价" << endl;    }};class student :public person{    virtual void Buyticket()    {        cout << "学生-半票" << endl;    }};void func(person p) //参数与程序一不同{    p.Buyticket();}int main(){    person p;    student s;    func(p);    func(s);    system("pause");    return 0;}

运行结果是:
成人-全票
成人-全票

程序二与程序一不同的是,程序二的参数成为了值传递,不再是引用于指针,不再构成重载,这样调用结果自然就由对象去决定,p是基类所构建出来的对象,所以调用基类的方法自然就不难理解了。

下面介绍多态的底层实现
多态的实现主要依赖虚表(虚函数表)通过一块连续的内存存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在⼀张虚函数表,虚函数表就像⼀张地图,指明了实际应该调⽤的虚函数函数。

下面先简单的通过一个简单的例子认识下:(在win10,vs2013下的演示情况,32位)

#include<iostream>using namespace std;class Base{public:    virtual void func1()    {}    virtual void func2()    {}private:    int a;};void Test1(){    Base b1;    cout << sizeof(b1) << endl;}int main(){    Test1();    system("pause");    return 0;}

通过sizeof()求出b1的类型大小是8
为什么?
在base类中我们只是定义了一个整形变量,应该只有4个字节,但是却求出了8个字节。打开监视窗口:
这里写图片描述
可以看出b1内还存在了一个指针,这个指针指向的位置呢就是虚基表的位置,指针的类型为函数指针数组的指针,他存储了指向虚函数的指针,虚基表内有所有的虚函数指针。
下图更加详细的展示了这一点:
这里写图片描述

下面呢介绍单继承的对象模型
同样先看代码:

#include<iostream>using namespace std;class Base{public:    virtual void func1()    {        cout << "Base::func1" << endl;    }    virtual void func2()    {        cout << "Base::func2" << endl;    }private:    int a;};class Derive :public Base{public:    virtual void func1()    {        cout << "Derive::func1" << endl;    }    virtual void func3()    {        cout << "Derive::func3" << endl;    }    virtual void func4()    {        cout << "Derive::func4" << endl;    }private:    int b;};//自己实现打印虚表typedef void(*FUNC)();void PrintVtable(int Vatable){    int* pVatable = (int*)Vatable;    cout << "虚表地址:" << pVatable << endl;    size_t i = 0;    for (i = 0; pVatable[i] != 0; i++)    {        printf("虚函数地址:%p->", pVatable[i]);        FUNC f = (FUNC)pVatable[i];        f();        printf("\n");    }    cout << endl;}int main(){    Base b;    Derive d;    int Vtab_b = *((int*)&b);    PrintVtable(Vtab_b);    int Vtab_d = *((int*)&d);    PrintVtable(Vtab_d);    system("pause");    return 0;}

同样先展示内存监视:
这里写图片描述
看基类的虚函数表是没有问题的,但是再看派生类的虚函数时发现只有两个,但我写了四个,看到的两个分别是对基类fun1()的重写,和继承基类的fun2();但自己写的fun3()与fun4()并没有。这块呢可以理解为V自身的问题(BUG)。
在上述代码中我写了虚表的打印函数,看运行结果:
这里写图片描述
从我们写的打印函数可以看出子类确实是存在4个虚函数的。
再看下对象模型:
这里写图片描述

多继承的对象模型:

#include<iostream>using namespace std;class Base1{public:    virtual void func1()    {        cout << "Base1::func1" << endl;    }    virtual void func2()    {        cout << "Base1::func2" << endl;    }private:    int b1;};class Base2{public:    virtual void func1()    {        cout << "Base2::func1" << endl;    }    virtual void func2()    {        cout << "Base2::func2" << endl;    }private:    int b2;};class Derive : public Base1, public Base2{public:    virtual void func1()    {        cout << "Derive::func1" << endl;    }    virtual void func3()    {        cout << "Derive::func3" << endl;    }private:    int d1;};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;}void Test1(){    Derive d1;    int* VTable = (int*)(*(int*)&d1);    PrintVTable(VTable);    // Base2虚函数表在对象Base1后⾯    VTable = (int *)(*((int*)&d1 + sizeof (Base1) / 4));    PrintVTable(VTable);}int main(){    Test1();    system("pause");    return 0;}

然后看对象模型:
这里写图片描述
观察对象模型可以发现,在多继承时,自身的虚函数,放在了第一个父类的虚表里面。

现在我们可以明白多态在实现的过程中借助了虚表这样的方式,或许还是会有疑惑它借助了虚表没错,但它是如何去用的呢?
看下面的代码:

#include<iostream>using namespace std;class person{public:    virtual void Buyticket()    {        cout << "成人-全价" << endl;    }};class student :public person{    virtual void Buyticket()    {        cout << "学生-半票" << endl;    }};void func(person& p){    p.Buyticket();  //A}int main(){    student s;    func(s);    return 0;}

在A处打断点,并转到反汇编

p.Buyticket();002058FE  mov         eax,dword ptr [p]  00205901  mov         edx,dword ptr [eax]  00205903  mov         esi,esp  //检查越界 00205905  mov         ecx,dword ptr [p]  00205908  mov         eax,dword ptr [edx]  0020590A  call        eax  0020590C  cmp         esi,esp //检查越界 0020590E  call        __RTC_CheckEsp (0201343h)  //检查越界 }

对比非多态的反汇编(程序二)

    p.Buyticket();00B9595E  lea         ecx,[p]  00B95961  call        person::Buyticket (0B91005h)  }

可以明显看出多态的调用指令更多,这里p运行时是直接去调指向对象的虚表。当指针p指向不同的对象时,就调用对应的虚表,虚表内的存的时函数指针
这就是它的底层实现。
通过虚表去认识多态,才能更好的理解多态。

可以思考下面的问题:
在构造函数与析构函数期间,能否调用虚函数?
不能。
构造函数期间,初始化没有完成,对象没有构造好,然后调虚函数,可能会引起越界访问的问题。
析构函数也是同样的原因。

内联函数,构造函数,静态成员函数能否写成虚函数?
不能。
内联函数是展开,并不存在函数地址。
构造函数对象没有初始化完成,虚函数时根据对象的类型来决定调父类还是子类的,对象都没有产生,如何调用虚函数。
静态成员函数是整个类所共有的,并不属于某个对象所以也不需要去动态的进行绑定。

深度探索:
菱形继承的对象模型:

#include <iostream>using namespace std;class A{public:    virtual void func1()    {        cout << "A::func1()" << endl;    }    virtual void func2()    {        cout << "A::func2()" << endl;    }public :    int _a;};class B :public A{public:    virtual void func1()    {        cout << "B::func1()" << endl;    }    virtual void func3()    {        cout << "B::func3()" << endl;    }public:    int _b;};class C :public A{public:    virtual void func1()    {        cout << "C::func1()" << endl;    }    virtual void func4()    {        cout << "C::func4()" << endl;    }public:    int _c;};class D : public B, public C{public:    virtual void func1()    {        cout << "D::func1()" << endl;    }    virtual void func5()    {        cout << "D::func5()" << endl;    }public:    int _d;};typedef void(*FUNC) ();void PrintVTable(int* VTable){    cout << " 虚表地址>" << VTable << endl;    int** ppVTable = (int**)VTable;    for (int i = 0; VTable[i] != 0; ++i)    {        printf(" 第%d个虚函数地址 :0X%p->", i, VTable[i]);        FUNC f = (FUNC)VTable[i];        f();    }    cout << endl;}void Test1(){    D d;    d.B::_a = 1;    d._b = 2;    d.C::_a = 3;    d._c = 4;    d._d = 5;    PrintVTable(*((int**)&d));    PrintVTable(*((int**)((char*)&d + sizeof(B))));}int main(){    Test1();    system("pause");    return 0;}

然后看它的对象模型:
这里写图片描述
观察菱形继承可以发现,它的本质仍然是一个多继承,它也存在菱形继承所特有的问题,代码的二义性和数据的冗余。冗余体现在虚函数的冗余和数据的冗余。

提到菱形继承自然就应该想到虚继承,虚继承是c++专门为解决菱形继承所设计的。
下面来探索菱形继承的虚继承的对象模型

#include <iostream>using namespace std;class A{public:    virtual void func1()    {        cout << "A::func1()" << endl;    }    virtual void func2()    {        cout << "A::func2()" << endl;    }public :    int _a;};class B :virtual public A{public:    virtual void func1()    {        cout << "B::func1()" << endl;    }    virtual void func3()    {        cout << "B::func3()" << endl;    }public:    int _b;};class C :virtual public A{public:    virtual void func1()    {        cout << "C::func1()" << endl;    }    virtual void func4()    {        cout << "C::func4()" << endl;    }public:    int _c;};class D : public B, public C{public:    virtual void func1()    {        cout << "D::func1()" << endl;    }    virtual void func5()    {        cout << "D::func5()" << endl;    }public:    int _d;};typedef void(*FUNC) ();void PrintVTable(int* VTable){    cout << " 虚表地址>" << VTable << endl;    int** ppVTable = (int**)VTable;    for (int i = 0; VTable[i] != 0; ++i)    {        printf(" 第%d个虚函数地址 :0X%p->", i, VTable[i]);        FUNC f = (FUNC)VTable[i];        f();    }    cout << endl;}void Test1(){    D d;    d.B::_a = 1;    d._b = 2;    d.C::_a = 3;    d._c = 4;    d._d = 5;}int main(){    Test1();    system("pause");    return 0;}

看它的对象模型:
这里写图片描述
分析对象模型可以得出:首先对象A,B,C各自都有自己的虚表,显然模型上的A部分就是公共区域,有公共区域,自然就应该有虚继承的内容,在上篇博客演示虚继承时,虚继承存了偏移量,这里分析一下B的虚基表。
有两行内容,第二行存了数据的偏移量,第一行自然是为了指向虚表的位置,它呢是在数据的偏移量加上它存储的偏移量(是一个有符号数,表中的存储是 -4),自然就找到虚表的位置。

可以察觉菱形继承虚继承的对象模型是复杂的,复杂就造成它的访问效率较低,故此它一般很少使用。
以上就是多态的内容。

备注:
1.虚函数与虚继承没有联系,它两只是使用了同一个关键字。虚函数是为了实现重写继而实现多态产生的,虚继承是为了解决菱形继承二义性与数据冗余产生的。
2.上述打印虚表的函数仅仅适用于32位的平台,下面给出,适用于32与64位平台的解决方案

typedef void(*FUNC) ();void PrintVTable(int* VTable){    cout << " 虚表地址>" << VTable << endl;    int** ppVTable = (int**)VTable;    for (int i = 0; VTable[i] != 0; ++i)    {        printf(" 第%d个虚函数地址 :0X%p->", i, VTable[i]);        FUNC f = (FUNC)VTable[i];        f();    }    cout << endl;}void Test1(){    D d;    d.B::_a = 1;    d._b = 2;    d.C::_a = 3;    d._c = 4;    d._d = 5;    PrintVTable(*((int**)&d));    PrintVTable(*((int**)((char*)&d + sizeof(B))));}

3.在成员函数的形参后⾯写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派⽣类中重新定义以后,派⽣类才能实例化出对象。
4.友元关系不能继承,也就是说基类友元不能访问⼦类私有和保护成员.
5.基类定义了static成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个⼦类,都只有⼀个static成员实例。

至此,本篇博客结束,建议与上篇博客继承一起阅读。