C++对象模型之简述C++对象的内存分布

在C++中,有两种类的成员变量:static和非static,有三种成员函数:static、非static和virtual。那么,它们如何影响C++的对象在内存中的分布呢? 当存在继承的情况下,其内存分布又是如何呢?

下面就一个非常简单的类,通过逐渐向其中加入各种成员,来逐一分析上述两种成员变量及三种成员函数对类的对象的内存分布的影响。

注:以下的代码的测试结果均是基于Ubuntu 14.04 64位系统下的G++ 4.8.2,若在其他的系统上或使用其他的编译器,可能会运行出不同的结果。

1、含有static成员变量及成员函数的类的对象的内存分布

类Persion的定义如下:

class Person
{
    public:
        Person():mId(0), mAge(20){}
        void print()
        {
            cout << "id: " << mId
                 << ", age: " << mAge << endl;
        }
    private:
        int mId;
        int mAge;
}; 

Person类包含两个非static的int型的成员变量,一个构造函数和一个非static成员函数。为弄清楚该类的对象的内存分布,对该类的对象进行一些操作如下:

int main()
{
    Person p1;
    cout << "sizeof(p1) == " << sizeof(p1) << endl;
    int *p = (int*)&p1;
    cout << "p.id == " << *p << ", address: "  << p << endl;
    ++p;
    cout << "p.age == " << *p << ", address: " << p << endl;
    cout << endl;

    Person p2;
    cout << "sizeof(p2) == " << sizeof(p1) << endl;
    p = (int*)&p2;
    cout << "p.id == " << *p << ", address: " << p << endl;
    ++p;
    cout << "p.age == " << *p << ", address: " << p << endl;
    return 0;
} 

其运行结果如下:

从上图可以看到类的对象的占用的内存均为8字节,使用普通的int*指针可以遍历输出对象内的非static成员变量的值,且两个对象中的相同的非static成员变量的地址各不相同。

据此,可以得出结论,在C++中,非static成员变量被放置于每一个类对象中,非static成员函数放在类的对象之外,且非static成员变量在内存中的存放顺序与其在类内的声明顺序一致。即person对象的内存分布如下图所示:

2、含有static和非static成员变量和成员函数的类的对象的内存分布

向Person类中加入一个static成员变量和一个static成员函数,如下:

class Person
{
     public:
         Person():mId(0), mAge(20){ ++sCount; }
         ~Person(){ --sCount; }
         void print()
         {
             cout << "id: " << mId
                  << ", age: " << mAge << endl;
         }
         static int personCount()
         {
             return sCount;
         }
     private:
         static int sCount;
         int mId;
         int mAge;
}; 

测试代码不变,与第1节中的代码相同。其运行结果不变,与第1节中的运行结果相同。

据此,可以得出:static成员变量存放在类的对象之外,static成员函数也放在类的对象之外。

其内存分布如下图所示:

3、加入virtual成员函数的类的对象的内存分布

在Person类中加入一个virtual函数,并把前面的print函数修改为函数,如下:

class Person
{
    public:
        Person():mId(0), mAge(20){ ++sCount; }
        static int personCount()
        {
            return sCount;
        }

        virtual void print()
        {
            cout << "id: " << mId
                 << ", age: " << mAge << endl;
        }
        virtual void job()
        {
            cout << "Person" << endl;
        }
        virtual ~Person()
        {
            --sCount;
            cout << "~Person" << endl;
        }

    protected:
        static int sCount;
        int mId;
        int mAge;
};

为了查看类的对象的内存分布,对类的对象执行如下的操作代码,如下:

int main()
{
    Person person;
    cout << sizeof(person) << endl;
    int *p = (int*)&person;
    for (int i = 0; i < sizeof(person) / sizeof(int); ++i, ++p)
    {
        cout << *p << endl;
    }
    return 0;
} 

其运行结果如下:

从上图可以看出,加virtual成员函数后,类的对象的大小为16字节,增加了8。通过int*指针遍历该对象的内存,可以看到,最后两行显示的是成员数据的值。

C++中的虚函数是通过虚函数表(vtbl)来实现,每一个类为每一个virtual函数产生一个指针,放在表格中,这个表格就是虚函数表。每一个类对象会被安插一个指针(vptr),指向该类的虚函数表。vptr的设定和重置都由每一个类的构造函数、析构函数和复制赋值运算符自动完成。

由于本人的系统是64位的系统,一个指针的大小为8字节,所以可以推出,在本人的环境中,类的对象的安插的vptr放在该对象所占内存的最前面。其内存分布图如下:

注:虚函数的顺序是按虚函数定义顺序定义的,但是它还包含其他的一些字段,本人还未明白它是什么,在下一节会详细说明虚函数表的内容。

4、虚函数表(vtbl)的内容及函数指针存放顺序

在第3节中,我们可以知道了指向虚函数表的指针(vptr)在类中的位置了,而函数表中的数据都是函数指针,于是便可利用这点来遍历虚函数表,并测试出虚函数表中的内容。

测试代码如下:

typedef void (*FuncPtr)();
int main()
{
    Person person;
    int **vtbl = (int**)*(int**)&person;
    for (int i = 0; i < 3 && *vtbl != NULL; ++i)
    {
        FuncPtr func = (FuncPtr)*vtbl;
        func();
        ++vtbl;
    }

    while (*vtbl)
    {
        cout << "*vtbl == " << *vtbl << endl;
        ++vtbl;
    }
    return 0;
}

代码解释:
由于虚函数表位于对象的首位置上,且虚函数表保存的是函数的指针,若把虚函数表当作一个数组,则要指向该数组需要一个双指针。我们可以通过如下方式获取Person类的对象的地址,并转化成int**指针:

Person person;
int **p = (int**)&person;

再通过如下的表达式,获取虚函数表的地址:

int **vtbl = (int**)*p;

然后,通过如下语句获得虚函数表中函数的地址,并调用函数。

FuncPtr func = (FuncPtr)*vtbl;
func();

最后,通过++vtbl可以得到函数表中下一项地址,从而遍历整个虚函数表。

其运行结果如下图所示:

从上图可以看出,遍历虚函数表,并根据虚函数表中的函数地址调用函数,它先调用print函数,再调用job函数,最后调用析构函数。函数的调用顺序与Person类中的虚函数的定义顺序一致,其内存分布与第3节中的对象内存分布图相一致。从代码和运行结果,可以看出,虚函数表以NULL标志表的结束。但是虚函数表中还含有其他的数据,本人还没有清楚其作用。

5、继承对于类的对象的内存分布的影响

本文并不打算详细地介绍继承对对象的内存分布的影响,也不介绍虚函数的实现机制。这里主要给出一个经过本人测试的大概的对象内存模型,由于代码较多,不一一贴出。假设所有的类都有非static的成员变量和成员函数、static的成员变量及成员函数和virtual函数。

1)单继承(只有一个父类)
类的继承关系为:class Derived : public Base



Derived类的对象的内存布局为:虚函数表指针、Base类的非static成员变量、Derived类的非static成员变量。

2)多重继承(多个父类)
类的继承关系如下:class Derived : public Base1, public Base2



Derived类的对象的内存布局为:基类Base1子对象和基类Base2子对象及Derived类的非static成员变量组成。基类子对象包括其虚函数表指针和其非static的成员变量。

3)重复继承(继承的多个父类中其父类有相同的超类)

类的继承关系如下:

class Base1 : public Base

class Base2:  public Base

class Derived : public Base1, public Base2


Derived类的对象的内存布局与多继承相似,但是可以看到基类Base的子对象在Derived类的对象的内存中存在一份拷贝。这样直接使用Derived中基类Base的相关成员时,就会引发歧义,可使用多重虚拟继承消除之。

4)多重虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

类的继承关系如下:
class Base1 : virtual public Base
class Base2:  virtual public Base
class Derived : public Base1, public Base2


Derived类的对象的内存布局与重复继承的类的对象的内存分布类似,但是基类Base的子对象没有拷贝一份,在对象的内存中仅存在在一个Base类的子对象。但是它的非static成员变量放置在对象的末尾处。

时间: 2024-10-05 07:20:02

C++对象模型之简述C++对象的内存分布的相关文章

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

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

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

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

C++对象的内存分布和虚函数表

c++中一个类中无非有四种成员:静态数据成员和非静态数据成员,静态函数和非静态函数. 1.非静态数据成员被放在每一个对象体内作为对象专有的数据成员.    2.静态数据成员被提取出来放在程序的静态数据区内,为该类所有对象共享,因此只存在一份.    3.静态和非静态成员函数最终都被提取出来放在程序的代码段中并为该类所有对象共享,因此每一个成员函数也只能存在一份代码实体.在c++中类的成员函数都是保存在静态存储区中的 ,那静态函数也是保存在静态存储区中的,他们都是在类中保存同一个惫份.    因此

C++对象内存分布(3) - 菱形继承(virtual)

1.前言 本篇文章的所有代码例子,如果是windows上编译运行,则使用的是visual studio 2013.如果是RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)上编译运行,则其gcc版本为4.4.7,如下所示: [[email protected] ~]# gcc --version gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4) 2.菱形继承类的内存分布 本篇文章主要讨论的是虚继承(virtual)下的内存分

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

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

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

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

《深度探索c++对象模型》chapter1关于对象对象模型

在c++中,有2种class data member:static和nostatic,以及3钟class member function:static,nostatic和virtual.已知下面这个class Point声明: class Point { public: Point(float xval); virtual ~Point(); float x() const; static int PointCount(); protected: virtual ostream& print(o

jvm学习记录-对象的创建、对象的内存布局、对象的访问定位

简述 今天继续写<深入理解java虚拟机>的对象创建的理解.这次和上次隔的时间有些长,是因为有些东西确实不好理解,就查阅各种资料,然后弄明白了才来做记录. (此文中所阐述的内容都是以HotSpot虚拟机为例的.) 对象的创建 java程序在运行过程中无时无刻都有对象被创建出来,那么创建对象是个怎么样的过程呢?还是看看我自己的理解吧. 判断是否已经执行类加载 当虚拟机遇到一条new指令时 ,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载

39-oc集合中对象的内存管理

集合中对象的内存管理 集合的内存管理原则是什么 当把一个对象添加到集合中时,这个对象会做了一次retain操作,计数器会+1 当一个集合被销毁时,会对集合里面的所有对象做一次release操作,计数器会-1 当一个对象从集合中移除时,会对这个对象做一次release操作,计数器会-1 集合方法的普遍规律是什么 如果方法名是add\insert开头,那么被添加的对象,计数器会+1 如果方法名是remove\delete开头,那么被移除的对象,计数器-1