(转)c++对象内存分析4

前言    本章节是4个课题的最后一个,我们将讨论多重继承情况下,对象内存的布局。阅读本文,请思考下面的问题:当子类从多个基类继承,虚函数指针和成员变量将如何布局?编译器如何进行子类和基类之间类型转换?如果多个基类具有同样的虚函数,子类选择哪个实现来调用?如果子类重写该虚函数,那么它覆盖的是哪个基类的实现呢?
多重继承    我们将分析这样的例子:CFinal类继承自CBasic类和CBasic1类;CBasic类和CBasic1类都定义有虚函数add和minus;CBasic类和CBasic1类都定义有成员变量int i;子类CFinal重写了虚函数add;子类CFinal增加了新的虚函数AVG。类图如下:


    代码:
class CBasic
{
  public:
      CBasic()
      {
          Array=new int[2];
      }
      int i;
      int *Array;
      virtual int add(int a, int b)
      {
          return a+b;
      }
      virtual  int minus(int a, int b)
      {
          return a-b;
      }
     void HelloWorld()
     {
         cout<<"hello world"<<endl;
     }
};  
  
class CBasic1
{
public:
      virtual int add(int a, int b)
      {
          cout<<"CBasic1::Add"<<endl;
          return a+b;
      }
     virtual  int minus(int a, int b)
      {
          cout<<"CBasic1::Minus"<<endl;
          return a-b;
      }
     int i;
     int iBasic1;
};
  
class CFinal:public CBasic,public CBasic1
{          
     int add(int a, int b)
      {
          cout<<"CFinal::Add"<<endl;
          return a+b;
      }
     virtual  int AVG(int a, int b)
      {
          cout<<"CFinal::AVG"<<endl;
          return (a+b)/2;
      }
    int iFinal;
};
    构造CFinal类对象:
CFinal *f=new CFinal;

    我们还是用Watch窗口来观察对象的布局:



    我们发现,在Watch窗口中打印f->__vfptr是不允许的,这是因为f中有2个虚函数指针,编译器不知道你想引用的是哪一个,因此我们需要把f转换为基类类型才能打印__vfptr,对于成员变量int i也是同样的。通过对内存布局的观察,我们得到这样的CFinal类内存结构图:

    我们发现:
    1)CBasic类对象位于CFinal类对象的前端,相应的,CBasic类的虚函数指针位于CFinal类对象的最前端。这是由于在定义CFinal类时,我们把CBasic类写在前面,编译器把它作为主基类。因此,编译器将在CBasic类的虚函数表表尾增加一个元素,来储存子类新增加的虚函数AVG的地址(请参考分析(2)中关于虚函数AVG在虚函数表中的位置的分析)。
    2)CBasic1对象开始于紧接着CBasic对象结束的位置,CFinal类新增的成员变量存储在CFinal对象的尾端。
    3)对于子类CFinal重写的add方法,在CBasic的虚函数表和CBasic1的虚函数表中,对应的元素都重定向为指向CFinal类的实现。对于CBasic1类来说,这种重定向是通过Thunk技术来实现的(特指VC++。本文将不讨论Thunk技术)。
    下面我们讨论章节开始提出的问题,如果我们用f对象调用minus方法,哪个基类的实现会被调用?同样我们也尝试调用成员变量i,因为2个基类都定义了它。运行下面的代码:
int _tmain(int argc, _TCHAR* argv[])
{
    CFinal *f=new CFinal;  
    CBasic *b=(CBasic*)f;
    f->minus(4,3); //编译错误,错误码C2385
    int x=f->i;   //编译错误,错误码C2385    
    b->minus(4,3); //成功
    int y=b->i;   //成功   return 0;
}
  
    我们发现,直接调用minus函数或者i是不被允许的,因为编译器不知道你想调用的是哪个基类的实现!然而,通过类型转换来指定特定的基类再进行调用,则可以成功。
    下一个问题,编译器如何进行子类和基类间的类型转换。对于主基类CBasic来说,这不成问题,因为它位于CFinal对象的最前端,不需要进行指针调整。那么转换为CBasic1类型呢?编译器会在CFinal对象指针的基础上加上12字节,跳过CBasic类对象从而指向CBasic1对象。你可能有这样的问题,为什么编译器知道要加上12字节,而不是13,14字节呢?这是因为编译器知道CFinal对象的布局,它清楚的知道CBasic1对象在CFinal对象中的偏移地址。如果CBasic对象的长度改变了,比如长度增加到16,需要重新编译整个程序,这样使用了CFinal对象的部分在分配地址和类型转换时,也将做出相应的改变。
    关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。

结论    让我们试着为多重继承的情况做出结论(如果你对上面的内容重复一遍没有兴趣,跳过这段):
    当子类从2个(或多个)带虚函数的基类继承时
    1)子类中,主基类的对象内存位于子类对象内存的最前端,相应地,主基类的虚函数指针地址等于子类对象地址。
    2)子类新增加的虚函数,将在主基类的虚函数表尾增加新的元素元素,来指向其实现。
    3)子类中其他基类的对象位于紧接着主基类结束后的地址。
    4)子类新增的成员变量位于子类对象内存的尾端。
    5)在子类中重写(override)的基类虚函数,在其所有基类的虚函数表中,对应的元素都覆盖为指向子类新实现的地址(通过THUNK技术实现)。
    6)对于多个基类中都定义过的虚函数,如果子类没有重写它,子类对象是不能直接调用的,因为编译器不知道你希望调用的是具体哪个基类的实现。同样,在多个子类中定义的成员变量,也不能被子类对象直接调用。
    5)子类对象转换为基类类型时,除非是主基类,需要进行指针调整,指针加上若干字节,跳过位于其前端的其他基类占用的地址,从而指向需要转换的基类。编译器之所以精确的知道需要偏移的地址,因为它通过类定义清楚地知道子类对象的内存布局。
    6)关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。

其他情况   至此,本文已经把所有经典常见的对象布局情况进行了研究,下面我们再简要的看几个更复杂的情形。
1,菱形多重继承。CFinal类的基类CBasic类和CBasic1类有共同的基类CInitial。
    类图:

    分析:这种情况和Subject4中多重继承的情况没有不同。对于编译器来说,CFinal类对2个基类的处理方式没有因为他们有共同的基类而有什么不同,只是在CFinal类对象中的CBasic类和CBasic类的内部又都分别包含了一个CInitial类。我们在Subject4种得到的所有结论在此都是适用的。

2,主基类没有虚函数的多重继承。考虑在Subject4的情况中,把CBasic类中的2个虚函数去掉,使其没有虚函数,在定义CFinal时仍把CBasic写在前面作为主虚函数。
    类图:

    分析:这种情况下,虽然我们把CBasic写在前面,但是CFinal类事实上把CBasic1类,即带虚函数指针和虚函数表的基类作为主基类,把它布局在对象的最前端。内存结构图:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值