目录

前言

对象的影响因素

单一的一般继承(带成员变量、虚函数、虚函数覆盖)

多重继承(带成员变量、虚函数、虚函数覆盖)

重复继承

钻石型多重虚拟继承


前言

《c++虚函数表解析》没涉及到:有成员变量的情况、有重复继承的情况、有虚拟继承的情况、有钻石型虚拟继承的情况,这篇文章将会是《C++虚函数表解析》的一个续篇,也是一篇高级进阶的文章。

对象的影响因素

简而言之,我们一个类可能会有如下的影响因素:成员变量、虚函数(产生虚函数表)、单一继承(只继承于一个类)、多重继承(继承多个类)、重复继承(继承的多个父类中其父类有相同的超类)、虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

上述的东西通常是C++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们只讨论C++语言上的影响。

单一的一般继承(带成员变量、虚函数、虚函数覆盖)

我们假设有如下所示的一个继承关系,请注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而子类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。

C++内存对象布局_父类

  • 源代码

源程序如下,我们使用以下程序作为测试程序:(下面程序中,我使用了一个int** pVtab 来作为遍历对象内存布局的指针,这样,我就可以方便地像使用数组一样来遍历所有的成员包括其虚函数表了,在后面的程序中,我也是用这样的方法的,请不必感到奇怪)

class Parent {

    public:

        int iparent;

        Parent() :iparent(10) {}

        virtual void f() { cout << " Parent::f()" << endl; }

        virtual void g() { cout << " Parent::g()" << endl; }

        virtual void h() { cout << " Parent::h()" << endl; }

    };



    class Child : public Parent {

    public:

        int ichild;

        Child() :ichild(100) {}

        virtual void f() { cout << "Child::f()" << endl; }

        virtual void g_child() { cout << "Child::g_child()" << endl; }

        virtual void h_child() { cout << "Child::h_child()" << endl; }

    };



    class GrandChild : public Child {

    public:

        int igrandchild;

        GrandChild() :igrandchild(1000) {}

        virtual void f() { cout << "GrandChild::f()" << endl; }

        virtual void g_child() { cout << "GrandChild::g_child()" << endl; }

        virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }

    };



    typedef void(*Fun)(void);



    void test()

    {

        GrandChild gc;

        int** pVtab = (int**)&gc;

        Fun  pFun;

        cout << "[0] GrandChild::_vptr->" << endl;

        for (int i = 0; (Fun)pVtab[0][i] != NULL; i++) {

            pFun = (Fun)pVtab[0][i];

            cout << "    [" << i << "] ";

            pFun();

        }

        cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;

        cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;

        cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;

    }
  • 运行结果及调试结果如下:

 

C++内存对象布局_父类_02

实际内存对象模型如下:

C++内存对象布局_多态_03

由此可知:虚函数表在最前面的位置,成员变量根据其继承和声明顺序依次放在后面,在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新

多重继承(带成员变量、虚函数、虚函数覆盖)

假设有下面这样一个类的继承关系。注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:

C++内存对象布局_c++_04

我们的类继承的源代码如下所示:父类的成员初始为10,20,30,子类的为100   

class Base1 {

    public:

        int ibase1;

        Base1() :ibase1(10) {}

        virtual void f() { cout << "Base1::f()" << endl; }

        virtual void g() { cout << "Base1::g()" << endl; }

        virtual void h() { cout << "Base1::h()" << endl; }

    };







    class Base2 {

    public:

        int ibase2;

        Base2() :ibase2(20) {}

        virtual void f() { cout << "Base2::f()" << endl; }

        virtual void g() { cout << "Base2::g()" << endl; }

        virtual void h() { cout << "Base2::h()" << endl; }

    };



    class Base3 {

    public:

        int ibase3;

        Base3() :ibase3(30) {}

        virtual void f() { cout << "Base3::f()" << endl; }

        virtual void g() { cout << "Base3::g()" << endl; }

        virtual void h() { cout << "Base3::h()" << endl; }

    };





    class Derive : public Base1, public Base2, public Base3 {

    public:

        int iderive;

        Derive() :iderive(100) {}

        virtual void f() { cout << "Derive::f()" << endl; }

        virtual void g1() { cout << "Derive::g1()" << endl; }

    };



    typedef void(*Fun)(void);

    void test()

    {

        Derive d;

        int** pVtab = (int**)&d;

        cout << "[0] Base1::_vptr->" << endl;

        Fun pFun = (Fun)pVtab[0][0];

        cout << "   [0] ";

        pFun();



        pFun = (Fun)pVtab[0][1];

        cout << "   [1] "; pFun();



        pFun = (Fun)pVtab[0][2];

        cout << "   [2] "; pFun();



        pFun = (Fun)pVtab[0][3];

        cout << "   [3] "; pFun();



        pFun = (Fun)pVtab[0][4];

        cout << "   [4] "; cout << pFun << endl;



        cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl;



        int s = sizeof(Base1) / 4;

        cout << "[" << s << "] Base2::_vptr->" << endl;

        pFun = (Fun)pVtab[s][0];

        cout << "   [0] "; pFun();



        pFun = (Fun)pVtab[s][1];

        cout << "   [1] "; pFun();



        pFun = (Fun)pVtab[s][2];

        cout << "   [2] "; pFun();



        pFun = (Fun)pVtab[s][3];

        cout << "   [3] ";

        cout << pFun << endl;





        cout << "[" << s + 1 << "] Base2.ibase2 = " << (int)pVtab[s + 1] << endl;



        s = s + sizeof(Base2) / 4;

        cout << "[" << s << "] Base3::_vptr->" << endl;

        pFun = (Fun)pVtab[s][0];

        cout << "   [0] "; pFun();



        pFun = (Fun)pVtab[s][1];

        cout << "   [1] "; pFun();



        pFun = (Fun)pVtab[s][2];

        cout << "   [2] "; pFun();



        pFun = (Fun)pVtab[s][3];

        cout << "   [3] ";

        cout << pFun << endl;



        s++;

        cout << "[" << s << "] Base3.ibase3 = " << (int)pVtab[s] << endl;

        s++;

        cout << "[" << s << "] Derive.iderive = " << (int)pVtab[s] << endl;

    }
  • 实际内存对象模型如下:

C++内存对象布局_多态_05

我们可以看到,每个父类都有自己的虚表;子类的成员函数被放到了第一个父类的表中;内存布局中,其父类布局依次按声明顺序排列;每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

重复继承

下面我们再来看看,发生重复继承的情况。所谓重复继承,也就是某个基类被间接地重复继承了多次,下图是一个继承图,我们重载了父类的f()函数。

C++内存对象布局_父类_06

其类继承的源代码如下所示。其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数。如子类D中,f()覆盖了超类的函数, f1() 和f2() 覆盖了其父类的虚函数,Df()为自己的虚函数。  

class B

    {

        public:

            int ib;

            char cb;

        public:

            B() :ib(0), cb('B') {}

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

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

    };



    class B1 : public B

    {

        public:

            int ib1;

            char cb1;

        public:

            B1() :ib1(11), cb1('1') {}

            virtual void f() { cout << "B1::f()" << endl; }

            virtual void f1() { cout << "B1::f1()" << endl; }

            virtual void Bf1() { cout << "B1::Bf1()" << endl; }

    };



    class B2 : public B

    {

        public:

            int ib2;

            char cb2;

        public:

            B2() :ib2(12), cb2('2') {}

            virtual void f() { cout << "B2::f()" << endl; }

            virtual void f2() { cout << "B2::f2()" << endl; }

            virtual void Bf2() { cout << "B2::Bf2()" << endl; }

    };



    class D : public B1, public B2

    {

        public:

            int id;

            char cd;

        public:

            D() :id(100), cd('D') {}

            virtual void f() { cout << "D::f()" << endl; }

            virtual void f1() { cout << "D::f1()" << endl; }

            virtual void f2() { cout << "D::f2()" << endl; }

            virtual void Df() { cout << "D::Df()" << endl; }

    };

    typedef void(*Fun)(void);

    void test()

    {

        int** pVtab = NULL;

        Fun pFun = NULL;



        D d;

        pVtab = (int**)&d;

        cout << "[0] D::B1::_vptr->" << endl;

        pFun = (Fun)pVtab[0][0];

        cout << "   [0] ";pFun();

        pFun = (Fun)pVtab[0][1];

        cout << "   [1] ";pFun();

        pFun = (Fun)pVtab[0][2];

        cout << "   [2] ";pFun();

        pFun = (Fun)pVtab[0][3];

        cout << "   [3] ";pFun();

        pFun = (Fun)pVtab[0][4];

        cout << "   [4] ";pFun();

        pFun = (Fun)pVtab[0][5];

        cout << "   [5] 0x" << pFun << endl;



        cout << "[1] B::ib = " << (int)pVtab[1] << endl;

        cout << "[2] B::cb = " << (char)pVtab[2] << endl;

        cout << "[3] B1::ib1 = " << (int)pVtab[3] << endl;

        cout << "[4] B1::cb1 = " << (char)pVtab[4] << endl;



        cout << "[5] D::B2::_vptr->" << endl;

        pFun = (Fun)pVtab[5][0];

        cout << "   [0] ";pFun();

        pFun = (Fun)pVtab[5][1];

        cout << "   [1] ";pFun();

        pFun = (Fun)pVtab[5][2];

        cout << "   [2] ";pFun();

        pFun = (Fun)pVtab[5][3];

        cout << "   [3] ";pFun();

        pFun = (Fun)pVtab[5][4];

        cout << "   [4] 0x" << pFun << endl;



        cout << "[6] B::ib = " << (int)pVtab[6] << endl;

        cout << "[7] B::cb = " << (char)pVtab[7] << endl;

        cout << "[8] B2::ib2 = " << (int)pVtab[8] << endl;

        cout << "[9] B2::cb2 = " << (char)pVtab[9] << endl;

        cout << "[10] D::id = " << (int)pVtab[10] << endl;

        cout << "[11] D::cd = " << (char)pVtab[11] << endl;

    }

C++内存对象布局_指针_07

我们可以看见,最顶端的父类B其成员变量存在于B1和B2中,并被D给继承下去了。而在D中,其有B1和B2的实例,于是B的成员在D的实例中存在两份,一份是B1继承而来的,另一份是B2继承而来的。所以,如果我们使用以下语句,则会产生二义性编译错误:

D d;

        d.ib = 0;               //二义性错误

        d.B1::ib = 1;           //正确

        d.B2::ib = 2;           //正确

注意:上面例程中的最后两条语句存取的是两个变量。虽然我们消除了二义性的编译错误,但B类在D中还是有两个实例,这种继承造成了数据的重复,我们叫这种继承为重复继承。重复的基类数据成员可能并不是我们想要的。所以,C++引入了虚基类的概念。

 

钻石型多重虚拟继承

虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的。钻石型的结构是其最经典的结构。也是我们在这里要讨论的结构:上述的“重复继承”只需要把B1和B2继承B的语法中加上virtual 关键,就成了虚拟继承,其继承图如下所示:

C++内存对象布局_c++_08

上图和前面的“重复继承”中的类的内部数据和接口都是完全一样的,只是我们采用了虚拟继承:其省略后的源码如下所示:

class B { …… };

        class B1 : virtual public B { …… };

        class B2 : virtual public B { …… };

        class D : public B1, public B2 { …… };
  • 单一虚拟继承

在查看D之前,我们先看一看单一虚拟继承的情况。下面是一段在VC++2019下的测试程序:   

class B

    {

        public:

            int ib;

            char cb;

        public:

            B() :ib(0), cb('B') {}

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

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

    };



    class B1 : virtual public B

    {

        public:

            int ib1;

            char cb1;

        public:

            B1() :ib1(11), cb1('1') {}

            virtual void f() { cout << "B1::f()" << endl; }

            virtual void f1() { cout << "B1::f1()" << endl; }

            virtual void Bf1() { cout << "B1::Bf1()" << endl; }

    };



    class B2 : virtual public B

    {

        public:

            int ib2;

            char cb2;

        public:

            B2() :ib2(12), cb2('2') {}

            virtual void f() { cout << "B2::f()" << endl; }

            virtual void f2() { cout << "B2::f2()" << endl; }

            virtual void Bf2() { cout << "B2::Bf2()" << endl; }

    };



    class D : public B1, public B2

    {

        public:

            int id;

            char cd;

        public:

            D() :id(100), cd('D') {}

            virtual void f() { cout << "D::f()" << endl; }

            virtual void f1() { cout << "D::f1()" << endl; }

            virtual void f2() { cout << "D::f2()" << endl; }

            virtual void Df() { cout << "D::Df()" << endl; }

    };

    typedef void(*Fun)(void);

    void test()

    {

        int** pVtab = NULL;

        Fun pFun = NULL;



        B1 bb1;

        pVtab = (int**)&bb1;

        cout << "[0] B1::_vptr->" << endl;

        pFun = (Fun)pVtab[0][0];

        cout << "     [0] ";

        pFun(); //B1::f1();

        cout << "     [1] ";

        pFun = (Fun)pVtab[0][1];

        pFun(); //B1::bf1();

        cout << "     [2] ";

        cout << pVtab[0][2] << endl;

        cout << "[1] = 0x";

        cout << (int*)*((int*)(&bb1) + 1) << endl; //B1::ib1

        cout << "[2] B1::ib1 = ";

        cout << (int)*((int*)(&bb1) + 2) << endl; //B1::ib1

        cout << "[3] B1::cb1 = ";

        cout << (char)*((int*)(&bb1) + 3) << endl; //B1::cb1

        cout << "[4] = 0x";

        cout << (int*)*((int*)(&bb1) + 4) << endl; //NULL

        cout << "[5] B::_vptr->" << endl;

        pFun = (Fun)pVtab[5][0];

        cout << "     [0] ";

        pFun(); //B1::f();

        pFun = (Fun)pVtab[5][1];

        cout << "     [1] ";

        pFun(); //B::Bf();

        cout << "     [2] ";

        cout << "0x" << (Fun)pVtab[5][2] << endl;

        cout << "[6] B::ib = ";

        cout << (int)*((int*)(&bb1) + 6) << endl; //B::ib

        cout << "[7] B::cb = ";

    }
  • 程序运行结果如下:

C++内存对象布局_指针_09

这里,大家可以自己对比一下。关于细节上,我会在后面一并再说。下面的测试程序是看子类D的内存布局,同样是VC++ 2019:

C++内存对象布局_指针_10

 

上面的输出结果中,我用不同的颜色做了一些标明,可以得到:

  1. 无论是GCC还是VC++,除了一些细节上的不同,其大体上的对象布局是一样的。也就是说,先是B1(黄色),然后是B2(绿色),接着是D(灰色),而B这个超类(青蓝色)的实例都放在最后的位置。
  2. 关于虚函数表,尤其是第一个虚表,GCC和VC++有很重大的不一样。但仔细看下来,还是VC++的虚表比较清晰和有逻辑性。
  3. VC++和GCC都把B这个超类放到了最后,而VC++有一个NULL分隔符把B和B1和B2的布局分开。GCC则没有。
  4. VC++中的内存布局有两个地址我有些不是很明白,在其中我用红色标出了。取其内容是-4。接道理来说,这个指针应该是指向B类实例的内存地址(这个做法就是为了保证重复的父类只有一个实例的技术)。但取值后却不是。这点我目前还并不太清楚,还向大家请教。解答:虚基类表中的第一个索引是自己的内存地址相对于自己存放指向虚基类表的地址的偏移量。第二个索引开始基类内存相对于它自己指向虚基类表地址的偏移量。 例如D中,Base1的虚基类表中的第一个索引中内容为-4,也就是在D的内存布局中,Base1地址相对于Base1虚基类表地址的偏移量,为-4,前面存放了一个指向虚函数表的指针;Base1虚基类表中的第二个索引为Base1的虚基类Base的地址相对于Base1虚基类表地址的偏移量,这里应该为36.
  5. GCC的内存布局中在B1和B2中则没有指向B的指针。这点可以理解,编译器可以通过计算B1和B2的size而得出B的偏移量。