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

转载自:https://blog.csdn.net/ljianhui/article/details/45903939#reply

在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. }   

其运行结果如下: 


从上图可以看出,加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++对象的内存布局

备注:上述代码在 linux 下测试无误,但是在 mac 下可能会出错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《深度探索C++对象模型》是由侯捷所著的一本经典的C++图书,该书于2012年由机械工业出版社出版。本书的主要内容涵盖了C++对象模型的深入解析和探讨。 在书中,作者详细讲解了C++中的对象模型和相关的概念,如类、对象、继承、多态等。作者首先介绍了C++对象模型的基本概念和特点,包括对象内存布局、虚函数表和虚函数指针等。然后,作者深入探讨了C++中的继承机制和多态性,包括单继承、多继承、虚继承等。作者还详细介绍了虚函数的实现原理和使用方法。 在书中,作者对C++对象模型的实现细节进行了深入的剖析,包括成员变量和成员函数内存布局函数指针和成员函数指针的用法等。同时,作者还讨论了C++中的一些高级特性,如模板、内存管理和异常处理等。通过对C++对象模型的深度探索,读者可以更好地理解C++的内部机制和原理,提高程序设计和开发能力。 《深度探索C++对象模型》适合具有一定的C++编程基础的读者阅读,尤其是对C++对象模型感兴趣的读者。通过阅读本书,读者可以进一步了解C++的底层实现和运行机制,从而提高自己的编程能力和代码质量。此外,本书还提供了大量的示例代码和实践案例,可以帮助读者更好地理解和应用所学知识。 总之,《深度探索C++对象模型》是一本深入探讨C++对象模型的经典著作,通过对C++的底层实现和内部机制的剖析,帮助读者深入理解C++编程语言,并提高自己的软件开发能力。 ### 回答2: 《深度探索C++对象模型》是由Stanley B. Lippman于1994年所著的一本经典畅销的C++书籍,该书详细介绍了C++对象模型的内部实现细节。 C++对象模型是指C++编译器在处理对象、继承、多态等面向对象特性时所采用的具体实现方式。这本书通过对对象模型的剖析,帮助读者深入理解C++的内部工作原理,从而写出更高效、更可靠的C++代码。 在《深度探索C++对象模型》中,作者首先介绍了对象、虚函数、继承等C++核心概念,然后详细讲解了C++对象模型的构建过程,包括对象布局、成员函数指针、虚函数表等。作者逐步深入地剖析了C++对象模型内存中的表示方式,解释了为什么C++可以支持如此强大的面向对象特性。 此外,本书还探讨了一些高级主题,如多重继承、虚拟继承、构造函数和析构函数的执行顺序等。对于想要深入学习C++的读者来说,这本书提供了一些宝贵的技术手册和实用的经验。 尽管《深度探索C++对象模型》的出版时间是1994年,但它仍然被广泛认可为学习C++对象模型的经典之作。在2012年时,由于C++的发展和演进,也许一些内容已经有些过时,但很多基本概念和原理仍然适用。 总而言之,《深度探索C++对象模型》是一本值得阅读的C++经典著作,通过深度探索C++对象模型,读者可以更加深入地了解C++的内部工作原理和实现方式,提升自己的开发技能。 ### 回答3: 《深度探索C++对象模型》是一本于2012年出版的书籍。该书的作者Andrews和Sorkin以全面的角度深入探讨了C++对象模型。该书重点介绍了C++中的对象表示、虚函数、继承、多重继承、构造函数、析构函数等内容,以及与之相关的语法、原理和底层实现。 这本书为读者揭示了C++对象模型的奥秘,让人更加深入地理解C++语言中的类和对象。作者通过分析对象布局、虚函数表、虚函数调用、多继承中的数据布局函数调用等等,解释了C++对象模型的实现机制。 在读者了解C++对象模型的基础上,该书还介绍了如何有效地利用对象模型来提高程序的性能。作者讨论了虚函数的成本以及如何减少虚函数调用的开销,提供了一些优化技巧。此外,书中还对C++的构造函数和析构函数进行了深入的讨论,详细解释了构造函数和析构函数的执行机制和注意事项。 总的来说,《深度探索C++对象模型》是一本深入剖析C++对象模型的重要参考书籍。通过阅读该书,读者可以更加全面地了解C++的类和对象的实现原理,对于理解C++语言的底层机制和优化程序性能具有积极的作用。无论是对于初学者还是有一定C++基础的开发人员来说,该书都是一本值得阅读的重要参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值