zoukankan      html  css  js  c++  java
  • C++内存中的封装、继承、多态(下)

    上篇讲述了内存中的封装模型,下篇我们讲述一下继承和多态。

    二、继承与多态情况下的内存布局

    由于继承下的内存布局以及构造过程很多书籍都讲得比较详细,所以这里不细讲。重点讲多态。

    继承有以下这几种情况:

    1.单一继承

    2.多重继承

    3.重复继承

    4.虚拟继承

    1.单一继承的场合

    假设有以下继承关系,那么大致的内存布局如下

    代码

    class Parent
    {
    public:
        
        int p;
    };
    
    class Child:public Parent
    {
    public:
    
        int c;
    
    };
    
    class GrandChild:public Child
    {
    public:
        
        int gc;
    };

    对象布局:

    成员变量的布局很好理解,那么在有虚函数的场合,虚函数表到底又是怎么样的呢?

    为了解决这个问题我完善上面的代码。

    class Parent
    {
    public:
        Parent():p(1){}
    
        virtual void fun1(){ cout<<"Parent::fun1"<<endl; }
        virtual void fun2(){ cout<<"Parent::fun2"<<endl; }
        virtual void fun3(){ cout<<"Parent::fun3"<<endl; }
        
        int p;
    };
    
    class Child:public Parent
    {
    public:
        Child():c(10){}
    
        virtual void fun1()  { cout<<"Child::fun1"<<endl; }
        virtual void c_fun2(){ cout<<"Child::c_fun2"<<endl; }
        virtual void c_fun3(){ cout<<"Child::c_fun3"<<endl; }
    
        int c;
    };
    
    class GrandChild:public Child
    {
    public:
        GrandChild():gc(100){}
    
        virtual void fun1()  { cout<<"GrandChild::fun1"<<endl; }
        virtual void c_fun2(){ cout<<"GrandChild::c_fun2"<<endl; }
        virtual void gc_fun3(){ cout<<"GrandChild::gc_fun3"<<endl; }
    
        int gc;
    };
    
    
    int main()
    {
        GrandChild grandc;
        Child       child;
        Parent     parent;
    
        return 0;
    }

    我们先使用调试窗口查看一下虚函数表

    可以看到三张表是不同的,可以看到fun1函数被改写了两次。

    比较蛋疼的是grandc只能看到三个函数,君不见c_fun2和gc_fun3,还得自己动手来。

    继上篇的内容,我们使用pf这个函数指针:

    typedef void (*PF)();
    PF pf = NULL;

    在主函数里我们写下代码:

        int* vtab = (int*)*(int*)&grandc;
    
        for (; *vtab != NULL; vtab++)
        {
            pf = (PF)*vtab;
            pf();
        }
    
        int* member = (int*)&grandc;
        cout<<*++member<<endl;
        cout<<*++member<<endl;
        cout<<*++member<<endl;

    成员变量输出结果与我们上篇的结论一致,咱们主要来看一下虚函数部分。

    并且前三个函数同调试窗口的显示结果。

    我们依据以上结果可以得到这么几个结论:

    1.单一继承时,不同的类维护不同的虚函数表(only one),并且虚函数表初始情况是父类的样子。

    2.当发生overwrite时,例如fun1和c_fun2都会冲刷掉父类的虚函数,代替之。

    3.没有发生overwrite时,直接添加到虚函数表中。

    图示:

    截止到这里,结合上篇的内容,就能很容易理解为什么使用父类指针能产生多态的效果了。

    2.多重继承的场合

    假设有以下继承关系,那么大致的内存布局如下

    由于是多继承,根据1的观点,单一继承时一个类维护一个虚函数表。多继承时怎么办呢?

    那只能是继承几个类,就有几张虚函数表了。

    实例代码如下:

    class Base1
    {
    public:
        Base1():b1(1){}
    
        virtual void fun1(){ cout<<"Base1::fun1"<<endl; }
        virtual void fun2(){ cout<<"Base1::fun2"<<endl; }
        virtual void fun3(){ cout<<"Base1::fun3"<<endl; }
        
        int b1;
    };
    
    class Base2
    {
    public:
        Base2():b2(2){}
        virtual void fun1(){ cout<<"Base2::fun1"<<endl; }
        virtual void fun2(){ cout<<"Base2::fun2"<<endl; }
        virtual void fun3(){ cout<<"Base2::fun3"<<endl; }    
    
        int b2;
    };
    
    class Base3
    {
    public:
        Base3():b3(3){}
        virtual void fun1(){ cout<<"Base3::fun1"<<endl; }
        virtual void fun2(){ cout<<"Base3::fun2"<<endl; }
        virtual void fun3(){ cout<<"Base3::fun3"<<endl; }
        
        int b3;
    };
    
    class Derived:public Base1, public Base2, public Base3
    {
    public:
        Derived():d(100){}
        virtual void fun1(){ cout<<"Derived::fun1"<<endl; }
        virtual void d_fun(){ cout<<"Derived::d_fun"<<endl; }
    
        int d;
    };

    通过调试窗口查看一下虚函数表:

    可以明确的看到标注了for base,源自哪个基类的虚函数表。

    并且可以看到fun1在三个表中全部被重写了,那么我们关心的d_fun到底会放在哪个表呢?

    我们使用相同的办法:

    typedef void (*PF)();
    PF pf = NULL;

        Derived dd;
    /////////////Base1///////////
        int* vtab1 = (int*)*(int*)&dd;
        for (; *vtab1 != NULL; vtab1++)
        {
            pf = (PF)*vtab1;
            pf();
        }
        int* member1 = (int*)&dd;
        cout<<*++member1<<endl;
    
    /////////////Base2///////////
        int* vtab2 = (int*)*((int*)&dd + sizeof(Base1)/4);
        for (; *vtab2 != NULL; vtab2++)
        {
            pf = (PF)*vtab2;
            pf();
        }
        int* member2 = (int*)((int*)&dd + sizeof(Base1)/4);
        cout<<*++member2<<endl;
    
    /////////////Base3//////////////
        int* vtab3 = (int*)*((int*)&dd + (sizeof(Base1)+sizeof(Base2))/4);
        for (; *vtab3 != NULL; vtab3++)
        {
            pf = (PF)*vtab3;
            pf();
        }
        int* member3 = (int*)((int*)&dd + (sizeof(Base1)+sizeof(Base2))/4);
        cout<<*++member3<<endl;

    偷了点懒,因为使用的是int型,所以没有存在字节对齐的情况,直接使用的sizeof/4,使用这种偏移量来访问不同的base区域。

    以下是输出结果:

    我们可以看到d_fun被放到了第一个函数表中去了(声明的次序的第一个,实例代码是base1的部分)。

    结论:

    1.多重继承的场合,overwirte时,父类的函数在三个表中会全部被重写。

    2.子类新添加的虚函数被放到第一个虚函数表中。

    图示:

    3.重复继承的场合

    其实重复继承只是多重继承的特例,一切的规则依然按照多重继承的规则实行。只是特殊在祖父类生成了两个拷贝镜像,形成数据重复,并且造成二义性。

    无论从设计的的角度还是维护的角度,这都是一个失败的选择。

    所以我们不重点讨论,直接跳到虚拟继承。

    4.虚拟继承的场合

    关于虚拟继承的对象模型,其实有多种方法,本文使用的的环境是vs2008,属于微软想的招儿。《深入C++对象模型》一书中明确指出了

    虚拟继承的场合,对象模型的构建方式没有固定的标准,主要的思路是拆分成不变局部和共享局部。当然只有更好的方法,也都是为了达到更高的存取效率。

    所以本文描述的内存布局或许只在微软编译器的场合成立,正因为如此,我们把重点放在虚拟继承的要达到的效果上。

    假设有以下继承关系:

    实例代码:

    class Base
    {
    public:
        Base():b(1){}
    
        virtual void fun(){   cout<<"Base::fun"<<endl; }
        virtual void B_fun(){ cout<<"Base::B_fun"<<endl; }
    
        int b;
    };
    
    class Base1:virtual public Base
    {
    public:
        Base1():b1(11){}
    
        virtual void fun(){    cout<<"Base1::fun"<<endl; }
        virtual void fun1(){   cout<<"Base1::fun1"<<endl; }
        virtual void B_fun1(){ cout<<"Base1::B_fun1"<<endl; }
        
        int b1;
    };
    
    class Base2:virtual public Base
    {
    public:
        Base2():b2(12){}
        virtual void fun(){    cout<<"Base2::fun"<<endl; }
        virtual void fun2(){   cout<<"Base2::fun2"<<endl; }
        virtual void B_fun2(){ cout<<"Base2::B_fun2"<<endl; }    
    
        int b2;
    };
    
    
    
    class Derived:public Base1, public Base2
    {
    public:
        Derived():d(111){}
        
        virtual void fun(){   cout<<"Derived::fun"<<endl; }
        virtual void fun1(){  cout<<"Derived::fun1"<<endl; }
        virtual void fun2(){  cout<<"Derived::fun2"<<endl; }
        virtual void D_fun(){ cout<<"Derived::D_fun"<<endl; }    
    
        int d;
    };

    先来讨论单一虚拟继承的情况,看一下Base1的布局:

    bb是Base的对象,bb1是Base1的对象。

    明显可以看到与普通单一继承不同,使用了两个虚函数指针,一个指向了虚基类Base的表,以及自己再生成一个表。

    而指向虚基类Base的表的虚函数fun明显被重写了。

    使用代码读取:

    int* vtab = (int*)*(int*)&bb1;
        for (; *vtab != NULL; vtab++)
        {
            pf = (PF)*vtab;
            pf();
        }

     

    这个循环运行会中断,原因是vtab访问了一个神奇的数字-4,这个是用来隔开的,不小心访问了。(陈皓老师的一篇博文《C++对象的内存布局》也遇到了相同的问题,而GCC却没有)

    足以证明,这里的不变局部是Derived自己后来添加的函数。而共享局部fun跑到虚基类包含的虚函数表上去了。

    我们使用二级指针来解决中断的问题。

    Base1 bb1;
    
        int** pVtab = (int**)&bb1;
    
        //////Base1//////////
        pf = (PF)pVtab[0][0];
        pf(); //Base1::fun1
        
        pf = (PF)pVtab[0][1];
        pf(); //Base1::B_fun1
    
        //cout << pVtab[0][2] << endl;//访问是一个随机值,证明越界了。
        cout << pVtab[1][0] << endl;//-4
    
        cout << (int)*((int*)(&bb1)+2) <<endl; //b1
    
        cout <<"0x"<<(int*)*((int*)(&bb1)+3) <<endl;//NULL 父类子类分隔处
    
        //////Base//////////
        pf = (PF)pVtab[4][0];
        pf();
        pf = (PF)pVtab[4][1];
        pf();
        cout << pVtab[4][2] << endl;//0x00
    
        cout << (int)*((int*)(&bb1)+5) <<endl; //b

    可以看出内存布局:

    1.不变布局(子类)放在对象模型的前端,共享布局(虚基类)放在尾端。

    2.其中子类部分,虚函数表使用了-4作为分隔结尾。接下来是子类成员变量值

    3.虚基类属于共享局部,是一个正常的虚函数表布局,并且重写了fun函数。

    图示:

    这样是能够保证共享部分处于虚基类中(包括虚函数表),不变部分处于子类中。

    接下来看完整的继承结构,解析Derived的布局。

    使用代码:

    Derived dd;
    
        int** pVtab = (int**)&dd;
    
        //////Base1//////////
        pf = (PF)pVtab[0][0];
        pf(); 
        
        pf = (PF)pVtab[0][1];
        pf();
    
        cout << pVtab[1][0] << endl;//-4
    
        cout << (int)*((int*)(&dd)+2) <<endl; //b1
    
        //////Base2//////////
        pf = (PF)pVtab[3][0];
        pf();
        pf = (PF)pVtab[3][1];
        pf();
    
        cout << pVtab[4][0] << endl;//-4
        cout << (int)*((int*)(&dd)+5) <<endl; //b2
    
        //////Derived 成员//////////
        cout << (int)*((int*)(&dd)+6) <<endl; //d
    
        //////NULL虚基类分隔//////////
        cout << "0x"<<(int*)*((int*)(&dd)+7) <<endl;
    
        pf = (PF)pVtab[8][0];
        pf();
        pf = (PF)pVtab[8][1];
        pf();
        cout << (int)*((int*)(&dd)+9) <<endl; //b

    运行结果:

    与单一虚拟继承类似:

    1.按照声明的次序,不变布局(父类)依次放在对象模型的前端,共享布局(虚基类)放在最尾端。

    2.其中不变布局部分,虚函数表使用了-4作为分隔结尾。接下来是子类成员变量值

    3.虚基类属于共享局部,是一个正常的虚函数表布局,并且重写了fun函数。

    图就不画了,与单一虚拟继承的情况类似。

    引用《深入C++对象模型》一书的描述:

    要在编译器中支持虚拟继承,困难度颇高。

    难度在于,要找到一个足够有效的办法,将Base1和Base2各自维护的Base部分,折叠成为一个由Derived单一维护的Base部分,并且还可以保持base class和Derived class的指针之间的多态操作。

    这也整是虚拟继承要达到的效果。

    至此,全篇差不多讲完了。

    主要参考书籍《深入C++对象模型》以及上文提到的陈皓老师的博文,内容稍长,难免有纰漏,望大家指正。

  • 相关阅读:
    《移动开发者周刊》第十一期
    2012安卓巴士开发者沙龙成都站大家抓紧报名
    23岁那年你正处在哪个状态?现在呢?
    《老罗Android开发视频教程》老罗来交国庆的答卷了
    程序员,你的一千万在哪里?
    《老罗Android开发视频教程》更新
    2012全球开发者大会项目投资一对一相亲会
    windows远程桌面
    [LeetCode] NQueens
    [LeetCode] Pascal's Triangle II
  • 原文地址:https://www.cnblogs.com/clor001/p/3329652.html
Copyright © 2011-2022 走看看