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

转帖自:

https://blog.csdn.net/ljianhui/article/details/45903939

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

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

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

1、含有非static成员变量及成员函数的类的对象的内存分布  
类Persion的定义如下: 
[cpp]  view plain  copy
  1. class Person  
  2. {  
  3.     public:  
  4.         Person():mId(0), mAge(20){}  
  5.         void print()  
  6.         {  
  7.             cout << "id: " << mId  
  8.                  << ", age: " << mAge << endl;  
  9.         }  
  10.     private:  
  11.         int mId;  
  12.         int mAge;  
  13. };   

Person类包含两个非static的int型的成员变量,一个构造函数和一个非static成员函数。为弄清楚该类的对象的内存分布,对该类的对象进行一些操作如下: 
[cpp]  view plain  copy
  1. int main()  
  2. {  
  3.     Person p1;  
  4.     cout << "sizeof(p1) == " << sizeof(p1) << endl;  
  5.     int *p = (int*)&p1;  
  6.     cout << "p.id == " << *p << ", address: "  << p << endl;  
  7.     ++p;  
  8.     cout << "p.age == " << *p << ", address: " << p << endl;  
  9.     cout << endl;  
  10.       
  11.     Person p2;  
  12.     cout << "sizeof(p2) == " << sizeof(p1) << endl;  
  13.     p = (int*)&p2;  
  14.     cout << "p.id == " << *p << ", address: " << p << endl;  
  15.     ++p;  
  16.     cout << "p.age == " << *p << ", address: " << p << endl;  
  17.     return 0;  
  18. }   

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

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



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

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

[cpp]  view plain  copy
  1. class Person  
  2. {  
  3.      public:  
  4.          Person():mId(0), mAge(20){ ++sCount; }  
  5.          ~Person(){ --sCount; }  
  6.          void print()  
  7.          {  
  8.              cout << "id: " << mId  
  9.                   << ", age: " << mAge << endl;  
  10.          }  
  11.          static int personCount()  
  12.          {  
  13.              return sCount;  
  14.          }  
  15.      private:  
  16.          static int sCount;  
  17.          int mId;  
  18.          int mAge;  
  19. };   

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

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

其内存分布如下图所示:

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

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

[cpp]  view plain  copy
  1. class Person  
  2. {  
  3.     public:  
  4.         Person():mId(0), mAge(20){ ++sCount; }  
  5.         static int personCount()  
  6.         {  
  7.             return sCount;  
  8.         }  
  9.   
  10.         virtual void print()  
  11.         {  
  12.             cout << "id: " << mId  
  13.                  << ", age: " << mAge << endl;  
  14.         }  
  15.         virtual void job()  
  16.         {  
  17.             cout << "Person" << endl;  
  18.         }  
  19.         virtual ~Person()  
  20.         {  
  21.             --sCount;  
  22.             cout << "~Person" << endl;  
  23.         }  
  24.   
  25.     protected:  
  26.         static int sCount;  
  27.         int mId;  
  28.         int mAge;  
  29. };  

为了查看类的对象的内存分布,对类的对象执行如下的操作代码,如下: 
[cpp]  view plain  copy
  1. int main()  
  2. {  
  3.     Person person;  
  4.     cout << sizeof(person) << endl;  
  5.     int *p = (int*)&person;  
  6.     for (int i = 0; i < sizeof(person) / sizeof(int); ++i, ++p)  
  7.     {  
  8.         cout << *p << endl;  
  9.     }  
  10.     return 0;  
  11. }   

第3行和第四行是一个8字节指针,是虚表指针,可见虚表指针放在对象的最开始。

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


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

由于本人的系统是64位的系统,一个指针的大小为8字节,所以可以推出,在本人的环境中,类的对象的安插的vptr放在该对象所占内存的最前面。其内存分布图如下:
注:虚函数的顺序是按虚函数定义顺序定义的,但是它还包含其他的一些字段,本人还未明白它是什么,在下一节会详细说明虚函数表的内容。





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

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



测试代码如下:
[cpp]  view plain  copy
  1. typedef void (*FuncPtr)();  
  2. int main()  
  3. {  
  4.     Person person;  
  5.     int **vtbl = (int**)*(int**)&person;  
  6.     for (int i = 0; i < 3 && *vtbl != NULL; ++i)  
  7.     {  
  8.         FuncPtr func = (FuncPtr)*vtbl;  
  9.         func();  
  10.         ++vtbl;  
  11.     }  
  12.   
  13.     while (*vtbl)  
  14.     {  
  15.         cout << "*vtbl == " << *vtbl << endl;  
  16.         ++vtbl;  
  17.     }  
  18.     return 0;  
  19. }  

代码解释:
由于虚函数表位于对象的首位置上,且虚函数表保存的是函数的指针,若把虚函数表当作一个数组,则要指向该数组需要一个双指针。我们可以通过如下方式获取Person类的对象的地址,并转化成int**指针:
[cpp]  view plain  copy
  1. Person person;  
  2. int **p = (int**)&person;  

再通过如下的表达式,获取虚函数表的地址:
 
[cpp]  view plain  copy
  1. int **vtbl = (int**)*p;  

然后,通过如下语句获得虚函数表中函数的地址,并调用函数。
[cpp]  view plain  copy
  1. FuncPtr func = (FuncPtr)*vtbl;  
  2. 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成员变量放置在对象的末尾处。

关于继承对对象的内存布局的影响以及虚函数的实现机制的详细介绍,请参阅——C++对象模型之详述C++对象的内存布局

猜你喜欢

转载自blog.csdn.net/bjzhaoxiao/article/details/80237947