美文网首页
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++对象的内存布局上篇

    这几天一直在看c++的基础东西,看到虚拟继承和虚函数这一块,总感觉了解的不深刻,所以在网上找了找博客,也自己动手敲...

  • NSObject 底层本质

    一、OC 转 C/C++ 二、NSObject 对象内存布局 三、NSObject 内存大小 四、OC 对象内存布...

  • C++ 对象内存布局

    虚函数, 虚基类 同时存在的时候, 对象内存布局的影响。 转自对象内存布局 (16) - CSDN博客 虚基类指针...

  • C++ 对象内存布局

    可能会对 C++ 对象的内存布局产生影响的因素: 对象的数据成员变量 对象的一般成员函数 对象的虚成员函数 对象继...

  • 深度探索C++对象模型(内存分布, 虚函数表)

    虽然C++面向对象很容易上手, 但是一直对C++对象的底层实现不知甚解, 得益于vs自带cl命令可以查看内存布局,...

  • iOS中OC对象的本质

    一个OC对象在内存中如何布局?以及一个NSObject对象占用多少内存? 我们知道OC的底层语言是c/c++我们平...

  • C++ 对象模型~内存布局

    //联系人:石虎QQ:1224614774昵称:嗡嘛呢叭咪哄 一、概念: 1.没有继承情况,vptr存放在对象的开...

  • java 内存布局

    Java 内存的布局主要是统计Java对象占用内存的大小。 Java对象的内存布局:对象头(Header)、实例数...

  • C++对象的内存布局下篇

    这篇博客是接上一篇,实际上本来是一篇的,但是我为了方便大家阅读,拆成了两篇,毕竟一篇特别长的博客实际上读起来非常的...

  • 一文详解 NSObject 对象的内存布局

    一文详解 NSObject 对象的内存布局一文详解 NSObject 对象的内存布局

网友评论

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

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