C++ 对象的内存布局(上)

转自陈皓的博客

前言

在谈论虚函数表里,至少有以下这些内容没有涉及:

- 有成员变量的情况。

- 有重复继承的情况。

- 有虚拟继承的情况。

- 有钻石型虚拟继承的情况。

所以,这篇文章将会是《C++虚函数表解析》的一个续篇,也是一篇高级进阶的文章。

对象的影响因素

简而言之,我们一个类可能会有如下的影响因素:

  1. 成员变量
  2. 虚函数(产生虚函数表)
  3. 单一继承(只继承于一个类)
  4. 多重继承(继承多个类)
  5. 重复继承(继承的多个父类中其父类有相同的超类)
  6. 虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

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

本篇文章着重讨论下述几个情况下的C++对象的内存布局情况。

  1. 单一的一般继承(带成员变量、虚函数、虚函数覆盖)
  2. 单一的虚拟继承(带成员变量、虚函数、虚函数覆盖)
  3. 多重继承(带成员变量、虚函数、虚函数覆盖)
  4. 重复多重继承(带成员变量、虚函数、虚函数覆盖)
  5. 钻石型的虚拟多重继承(带成员变量、虚函数、虚函数覆盖)

我们的目标就是,让事情越来越复杂。

知识复习

我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如:

            typedef void(*Fun)(void);

            Base b;

            Fun pFun = NULL;

            cout << "虚函数表地址:" << (int*)(&b) << endl;
            cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;

            // Invoke the first virtual function
            pFun = (Fun)*((int*)*(int*)(&b));
            pFun();

我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内存对象的布局。

本篇文章中的例程或内存布局主要使用如下编译器和系统:

1)Windows XPVC++ 2003

2)CygwinG++ 3.4.4

单一的一般继承

下面,我们假设有如下所示的一个继承关系:

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

我们的源程序如下所示:

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; }
};

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

    typedef void(*Fun)(void);

    GrandChild gc;

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

    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;

其运行结果如下所示:

[0] GrandChild::_vptr->
    [0] GrandChild::f()
    [1] Parent::g()
    [2] Parent::h()
    [3] GrandChild::g_child()
    [4] Child::h1()
    [5] GrandChild::h_grandchild()
[1] Parent.iparent = 10
[2] Child.ichild = 100
[3] GrandChild.igrandchild = 1000

使用图片表示如下:

可见以下几个方面:

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

多重继承

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

我们的类继承的源代码如下所示:父类的成员初始为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; }
};

我们通过下面的程序来查看子类实例的内存布局:下面程序中,注意我使用了一个s变量,其中用到了sizof(Base)来找下一个类的偏移量。(因为我声明的是int成员,所以是4个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)

                typedef void(*Fun)(void);

                Derive d;

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

                cout << "[0] Base1::_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] "; 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();

                Fun = (Fun)pVtab[s][1];
                cout << "     [1] "; pFun();

                pFun = (Fun)pVtab[s][2];
                cout << "     [2] "; pFun();

                pFun = (Fun)pVtab[s][3];
                out << "     [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;

其运行结果如下所示:(在VC++ 2003G++ 3.4.4下)

[0] Base1::_vptr->
     [0] Derive::f()
     [1] Base1::g()
     [2] Base1::h()
     [3] Driver::g1()
     [4] 00000000      ? 注意:在GCC下,这里是1
[1] Base1.ibase1 = 10
[2] Base2::_vptr->
     [0] Derive::f()
     [1] Base2::g()
     [2] Base2::h()
     [3] 00000000      ? 注意:在GCC下,这里是1
[3] Base2.ibase2 = 20
[4] Base3::_vptr->
     [0] Derive::f()
     [1] Base3::g()
     [2] Base3::h()
     [3] 00000000
[5] Base3.ibase3 = 30
[6] Derive.iderive = 100

使用图片表示是下面这个样子:

我们可以看到:

  1. 每个父类都有自己的虚表
  2. 子类的成员函数被放到了第一个父类的表中
  3. 内存布局中,其父类布局依次按声明顺序排列。
  4. 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
时间: 2024-11-08 21:09:55

C++ 对象的内存布局(上)的相关文章

C++ 对象的内存布局(上)

C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 点击这里查看下篇>>> 前言 07年12月,我写了一篇<C++虚函数表解析>的文章,引起了大家的兴趣.有很多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的.我在这里一并对大家的留言表示感谢.这也是我为什么再写一篇续言的原因.因为,在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的问题,也是为了图一些表达上方便和简单.不想,这篇文章成为了打开C++对象模

VS中C++对象的内存布局

本文主要简述一下在Visual Studio中C++对象的内存布局,这里没有什么测试代码,只是以图文的形式来描述一下内存分布,关于测试的代码以及C++对象模型的其他内容大家可以参考一下陈皓先生的几篇博文以及网上的其他一些文章: <C++虚函数表解析>:http://blog.csdn.net/haoel/article/details/1948051 <C++对象的内存布局(上)>:http://blog.csdn.net/haoel/article/details/3081328

Java对象的内存布局

Java对象的内存布局:对象头(Header),实例数据(Instance Data),对齐填充(Padding):另外:不同的环境结果可能有差异,我所在的环境是HotSpot虚拟机,64位Windows. 对象头 对象头在32位系统上占用8bytes,64位系统上占用16bytes. System.out.println("sizeOf(new Object()) = " + sizeOf(new Object())); sizeOf(new Object()) = 16 实例数据

c++对象模型是什么,对象的内存布局和结构问题

在c++发明的初期对于c++对象模型的争论从来没有停止过直到标准委员会通过了最终的c++对象模型这件事情才变得尘埃落定.C++对象模型可能是最不需要去解释的,但是又是不得不去说的因为c++的入门最先接触的就是c++对象.在上个世纪一共有三种c++对象模型,它们的出现可以说是一个不断优化的过程最终只有我们目前看到的c++对象模型在使用.了解c++对象模型非常重要,了解之后对于对象的内存布局,内存大小,虚函数以及静态数据成员和成员函数的理解有非常巨大的帮助.言归正传,下面就来分别讨论c++的三种对象

java对象的内存布局(一):计算java对象占用的内存空间以及java object layout工具的使用

最近在学习java对象内存布局方面的一些知识,主要是想知道一个java对象到底占用多少内存空间,以及java对象在内存中到底是什么样子的.c/c++中的sizeof运算符能够方便地告诉我们一个变量占用的内存空间,但是在java中却没有直接提供这种机制.如果想获取java对象占用的内存大小,可以利用java的Instrumentation机制.java.lang.instrument.Instrumentation这个接口提供了getObjectSize(Object objectToSize),

C++ 对象的内存布局(下)

重复继承 下面我们再来看看,发生重复继承的情况.所谓重复继承,也就是某个基类被间接地重复继承了多次. 下图是一个继承图,我们重载了父类的f()函数. 其类继承的源代码如下所示.其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数.如子类D中,f()覆盖了超类的函数, f1() 和f2() 覆盖了其父类的虚函数,Df()为自己的虚函数. class B { public: int ib; char cb; public: B

win x64下C++对象的内存布局的学习

这篇文章是在学习陈皓的一篇文章<C++对象的内存布局(上)>后,针对x64的下情况的学习笔记.他的文章的链接地址是:http://blog.csdn.net/haoel/article/details/3081328. 他的示例代码中有下列片段: 如果在vs2015社区版中以debug方式编译出x64版本的exe运行后会崩溃.经过调试发现在x64下地址是以unsigned long long表示的(如果测试发现,编译后台实际上会把unsigned long long 转换为unsigned _

C++对象模型之详述C++对象的内存布局

在C++对象模型之简述C++对象的内存布局一文中.详细分析了各种成员变量和成员函数对一个类(没有不论什么继承的)对象的内存分布的影响,及详细解说了怎样遍历对象的内存,包含虚函数表.假设你在阅读本文之前.还没有看过C++对象模型之简述C++对象的内存布局一文,建议先阅读一下.而本文主要讨论继承对于对象的内存分布的影响,包含:继承后类的对象的成员的布局.继承对于虚函数表的影响.virtual函数机制怎样实现.执行时类型识别等. 因为在C++中继承的关系比較复杂.所以本文会讨论例如以下的继承情况: 1

JVM——深入分析对象的内存布局

概述 一个对象本身的内在结构需要一种描述方式,这个描述信息是以字节码的方法存储在方法区中的.Class 本身就是一个对象,都以 KB 为单位,如果 new Integer() 为了表示一个数据就占用KB级别的内存就有点不值了,下面讲解 JVM 是如何做的.为了表示对象的属性.方法等信息,不得不需要结构描述.Hotspot VM 使用对象头部的一个指针指向 Class 区域的方式来找到对象的 Class 描述,以及内部的方法.属性入口.如下图所示: 在 HotSpot 虚拟机中,对象在内存中存储布