美文网首页
C++对象的内存布局上篇

C++对象的内存布局上篇

作者: lwj_ow | 来源:发表于2018-02-11 18:29 被阅读0次

    这几天一直在看c++的基础东西,看到虚拟继承和虚函数这一块,总感觉了解的不深刻,所以在网上找了找博客,也自己动手敲了敲看看结果,感觉理解加深了很多,在这里也总结一下,加深记忆也方便以后的回忆.

    我先贴一下我参考的两篇博客的地址,我的测试也是参考上面的代码的:
    C++对象的内存布局
    C++虚继承对象的内存布局

    我将上面两篇博客的代码在我的电脑上都测试了一下,结果放到下面,因为电脑的进步,我对它们的代码也做了一点修改,因为上面两篇博客的博主测试的时候电脑应该是32位的.我的测试平台是Ubuntu 16.04,编译器是版本gcc 5.4.0,电脑是64位.

    注意:这里我不多细说原理,主要是一些C++的基础知识指针类型转换什么的,我们代码的主要目的是为了测试虚继承或者拥有虚函数的对象的内存布局,总共有三个例子,我只描述一下类的结构,然后就直接贴上代码了,最后画出我根据测试结果画出来的内存布局图,不同平台上测试的结果很可能有细微的差别.另外值得注意的是,代码里的不少结果都是根据已知情况设定的值,比如一个类的virtual table中到底有几个函数指针,这个都是根据已知情况设定的值,另外,多说一句,如果遇到问题,我非常推荐用gdb进行调试,gdb非常的强大,我在测试代码的时候也遇到了问题,用的gdb调试,譬如如果我们打印一个指针的值,如果这个指针是一个函数指针的话,那么gdb会告诉我们这个指针指向哪个函数,这是非常有用的信息.如果读者在用我的代码测试的时候有任何疑惑的话,使用gdb调试即可,相信你绝对不会失望的.

    1. 第一个例子是单一继承,这个例子比较简单,不过也是为我们后面做个铺垫,大家稍微注意下即可.


      image.png

      下面贴一下代码,值得注意的是我把int都换成了long,因为在64位系统中,指针的大小是8字节,但是int的大小是4字节,所以如果我们使用int作为类的成员的话,可能会出现编译器产生内存对齐的情况,对我们的分析造成影响,所以我都换为了long,在64位系统下long的大小是8字节.

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    struct Parent
    {
          long parent;
          Parent():parent(10){}
          virtual void f(){ cout<<"Parent::f"<<endl; }
          virtual void g(){ cout<<"Parent::g"<<endl; }
          virtual void h(){ cout<<"Parent::h"<<endl; }
    };
    
    struct Child : public Parent 
    {
          long child;
          Child():child(20){}
          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; }
    };
    
    
    struct GrandChild : public Child 
    {
          long grandchild;
          GrandChild():grandchild(30){}
          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(*func)(void);
    
    int main()
    {
          GrandChild gc;
          long **pVtab = (long **)&gc;
          for(int i = 0;(func)pVtab[0][i] != NULL;i++)
          {
                  auto pFunc = (func)pVtab[0][i];
                  cout<<"["<<i<<"]";
                  pFunc();
          }
          cout<<(long)pVtab[1]<<endl;
          cout<<(long)pVtab[2]<<endl;
          cout<<(long)pVtab[3]<<endl;
          return 0;
    }
    

    下面贴一下结果图:


    image.png

    那么grandchild对象的内存布局就很明显了:


    image.png

    有这么几点需要注意一下:

    • 指向虚函数表的指针是在对象最开始的位置
    • 后面是存放成员变量的位置,这里因为我变量都使用的long,所以没有对齐的数据,否则可能还会有内存对齐的要求.
    • 在虚函数表中,由于这些都是public继承,且一路继承下来,所以这些类的成员函数都是放在一起的,否则我们在后面会看到,如果是virtual继承,virtual基类的成员函数和派生类的成员函数并不放在同一个虚函数表内.
    • 这里,我们需要注意一下函数在内存中的规律,乍一看似乎没有规律可言,因为派生类的成员函数和基类的成员函数是杂乱放在一起的,不过仔细看来我们就会发现这是按照继承和声明的顺序来放置的,最基类的位置地址最低,同一个类的函数位置根据声明顺序来确定,越早声明的地址越低.
    • 接上一个,这里基类的virtual函数由于被派生类重写了,所以虚函数表内是派生类的成员函数,譬如索引为0,3的函数都是被派生类重写的,所以放的是派生类的成员函数的地址,这也是虚函数动态绑定的原理.
    • -1的那个索引项是我用gdb试出来的,这个信息是用来实现RTTI的,有兴趣的朋友可以去搜一下看.
    1. 继续下一个例子,这个例子是多重继承,大概继承图如下:


      image.png

      这里是Derived类公有派生自Base1-3,并且只重写了f()函数,另外有一个自己的函数g1(),下面是代码,原理同上面一样.

    #include <iostream>
    using namespace std;
    struct Base1
    {
            long base1;
            Base1(): base1(10) {}
            virtual void f()
            {
                    cout << "Base1::f" << endl;
            }
            virtual void g()
            {
                    cout << "Base1::g" << endl;
            }
            virtual void h()
            {
                    cout << "Base1::h" << endl;
            }
    };
    struct Base2
    {
            long base2;
            Base2(): base2(20) {}
            virtual void f()
            {
                    cout << "Base2::f" << endl;
            }
            virtual void g()
            {
                    cout << "Base2::g" << endl;
            }
            virtual void h()
            {
                    cout << "Base2::h" << endl;
            }
    };
    struct Base3
    {
            long base3;
            Base3(): base3(30) {}
            virtual void f()
            {
                    cout << "Base3::f" << endl;
            }
            virtual void g()
            {
                    cout << "Base3::g" << endl;
            }
            virtual void h()
            {
                    cout << "Base3::h" << endl;
            }
    };
    struct Derived : public Base1, public Base2, public Base3
    {
            long derived;
            Derived(): derived(100) {}
            virtual void f()
            {
                    cout << "Derived::f" << endl;
            }
            virtual void g1()
            {
                    cout << "Derived::g1" << endl;
            }
    };
    typedef void (*func)(void);
    int main()
    {
            Derived d;
            long **pVtab = (long **)&d;
            for(int i = 0; i <= 3; i++)
            {
                    auto pFunc = (func)pVtab[0][i];
                    cout << "[" << i << "]";
                    pFunc();
            }
            cout << (long)pVtab[1] << endl;
            for(int i = 0; i <= 2; i++)
            {
                    auto pFunc = (func)pVtab[2][i];
                    cout << "[" << i << "]";
                    pFunc();
            }
            cout << (long)pVtab[3] << endl;
            for(int i = 0; i <= 2; i++)
            {
                    void (*pFunc)(void);
                    pFunc = (func)pVtab[4][i];
                    cout << "[" << i << "]";
                    pFunc();
            }
            cout << (long)pVtab[5] << endl;
            cout << (long)pVtab[6] << endl;
            return 0;
    }
    

    同样,我也是在测出大概内存布局的情况下知道一些参数的(笑),下面我贴一下运行的结果:


    image.png

    根据结果,Derived对象的内存布局就很明显了,我画了一个图来辅助大家理解:


    image.png

    从这个图来看,相信大家就很容易理解这个Derived对象的内存布局了.为了作图起见,我去掉了关于typeinfo的信息,这个和前面是一样的,都是在vptr指向的位置的前一个位置存放typeinfo的.
    从这里,我们也有几点需要注意的:

    • 这里每个父类都是有自己的虚表的,但是Derived会和第一个父类共享虚函数表,注意这个单一继承的相似性,类总是会和自己的父类以及更向上的祖先共享虚函数表的,但是如果有多个父类的话,就会选择第一个父类了.
    • 这里同单一继承相似,派生类的重写的函数会覆盖掉基类的函数,譬如图上的Derived::f()覆盖掉了所有基类的f()函数,这是实现多态的原理
    • 同单一继承很像,虚函数表顺序依然是先基类后派生类,先声明的函数靠前,后声明的函数在后面

    为了方便大家阅读,我将这篇又臭又长(笑)拆成了两篇.下篇就在后面,后面这个例子要稍微复杂一点,如果您看到了这里,可以休息一下再继续.

    相关文章

      网友评论

          本文标题:C++对象的内存布局上篇

          本文链接:https://www.haomeiwen.com/subject/riuktftx.html