C++ 多重继承和虚拟继承对象模型、效率分析

一、多态

C++多态通过继承和动态绑定实现。继承是一种代码或者功能的传承共享,从语言的角度它是外在的、形式上的,极易理解。而动态绑定则是从语言的底层实现保证了多态的发生——在运行期根据基类指针或者引用指向的真实对象类型确定调用的虚函数功能!通过带有虚函数的单一继承我们可以清楚的理解继承的概念、对象模型的分布机制以及动态绑定的发生,即可以完全彻底地理解多态的思想。为了支持多态,语言实现必须在时间和空间上付出额外的代价(毕竟没有免费的晚餐,更何况编译器是毫无感情):

1、类实现时增加了virtual table,用来存放虚函数地址;
2、类对象中增加了指向虚函数表的指针vptr,以提供runtime的链接;
3、在类继承层次的构造函数中重复设定vptr的初值,以期待指针指向对应类的virtual table;
4、在类继承层次的析构函数中重复还原vptr的初值;
5、多态发生时(base class指针调用虚函数)需要通过vptr和virtual table表调用对应函数实体,增加了 一层间接性。
第1、2两点是多态带来的空间代价,后面三点则是时间效率上的代价。

二、多重继承和虚拟继承

多重继承具有多个base class,有别于单一继承(提供了一种“自然多态”形式)。单一继承中,基类和派生类具有相同的内存地址,它们之间的转换十分自然不需要编译器的介入。但如果基类中没有虚函数而派生类中有,单一继承的自然多态被打破。这种情况下,派生类转换为基类需要编译器的介入,用以调整this指针地址。多重继承的对象模型较单一继承复杂,根源在于derived class objects和其第二或后继的base class objects之间的“非自然”关系 ,这一点可以从下面的对象模型中看到。派生类和基类之间的非自然多态引起了一个严重的问题(在虚拟继承中也存在):derived class和第二或后继base class之间的转换(不论是对象间的直接转换或者经由其所支持的virtual function机制做转换)需要调整this指针的地址,以使其指向完整正确的class object 。
虚拟继承是一种机制,类通过虚继承指出它所希望共享虚基类的状态,虚基类在派生层次中只有一份实体。相比多重继承,虚拟继承的难点在于既要识别出相同的对象部分又要维持基类和派生类之间的多态关系 。通常情况下,实现虚拟继承时编译器将对象分割为一个不变局部和一个共享局部 。不变局部中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以被直接存取。至于共享局部,所表现的就是virtual base class subobject。这一部分的数据,其位置会因为每次的派生操作而有变化,所以它们只能被间接存取 。各家编译器实现技术之间的差异在于间接存取方法不同。一般的策略就是先安排好派生类的不变部分,然后建立共享部分。虚拟继承base class和derived class之间非自然的多态关系,它们之间相互转换时需要对this指针地址进行调整。由于对virtual base class的支持,虚拟继承带来了额外的负担和模型复杂性。

三、多重继承和虚拟继承对象模型

造成多重继承和虚拟继承较普通单一继承复杂、效率低的本质在于 对象模型内存分布的差异, 这一点从第二部分分析也可以看到。下面示例对比列出了普通单一继承、多重继承以及虚拟继承的对象模型。需要说明的是:C++标准中并没有强制规定base class members和derived class members之间的次序关系,理论上可以自由安排之,但实际上大多数编译器都会基类成员放在前面,但虚拟继承除外。下面也是这种策略,同时把vptr作为类的第一个成员。

基类Base1、Base2以及派生类DerivedSingle、DerivedMulti类定义如下:

class Base1
{
public:
  Base1(void);
  ~Base1(void);
  virtual Base1* clone()const;
protected:
  float data_Base1;
};
class Base2
{
public:
  Base2(void);
  ~Base2(void);
  virtual void mumble();
  virtual Base2* clone()const;
protected:
  float data_Base2;
};
class DerivedSingle: public Base1
{
public:
  DerivedSingle(void);
  virtual ~DerivedSingle(void);
  virtual DerivedSingle* clone() const;
protectd:
  float data_DerivedSingle;
};
class DerivedMulti :public Base1, public Base2
{
public:
  DerivedMulti(void);
  virtual ~DerivedMulti(void);
  virtual DerivedMulti* clone() const;
protected:
  float data_DerivedMulti;
};

对象模型如下,虚拟继承和单一继承类结构相同,只是继承改成了虚拟继承。

单一继承

多重继承:

虚拟继承:

为了保证memberwise复制的正确性(否则基类子对象复制给派生类时会发生错误),C++中保证“基类子对象在派生类中的原样性 ”。

单一继承的对象模型呈现了一种“自然多态”的形式,基类和派生类之间的转换十分自然简单。然而多重继承有多个基类,对象有多个vptr指针,对于第二个或后继基类和派生类之间的转换需要地址调整,以指向完整的基类子对象。

虚拟继承中,为了记住和共享虚拟基类,需要在类中添加指向该基类的指针。从上面的虚拟继承对象模型中可以看到,虽然和单一继承有相同的类层次结构,但虚拟继承打破了单一继承的“自然多态”形式,基类和派生类之间的转换需要调整this指针的地址。如果是虚拟多重继承,则虚拟基类/后继基类和派生类之间的转换需要this指针地址调整 。

一般规则,多重继承经由指向“第二个或者后继base class”的指针(引用)来调用derived class virtual function,该操作所连带的“必要的this指针调整”操作,必须在执行期完成,也就是说offset的大小、以及吧offset加到this指针上头的那一小段程序代码,必须有编译器在某个地方插入。为了实现this指针调整引入thunk技术,所谓thunk是一小段assembly代码,用来以适当的offset值调整this指针,并跳到virtual函数去。Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要额外任何空间上的额外负担。Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针)。调整this指针的第二个额外负担就是,由于两中不同的可能:(1)经由derived class(或者第一个base class)调用,(2)经由第二个(或者后继)base class调用,同一个函数在virtual table中可能需要多笔对应的slots。并且在第二个或者后继base class中的虚函数表保存的是thunk代码地址。

四、 效率

通过上面第三部分的分析,多重继承和虚拟继承对象模型的较单一继承复杂的对象模型 ,造成了成员访问低效率, 表现在两个方面:对象构建时vptr的多次设定,以及this指针的调整。对于多种继承情况的效率比较如下:

情形 Vptr 设定 Data member 访问 virtual Function member 访问 效率分析
单一继承 no vptr 指针/引用/对象访问效率相同 直接访问 效率较高
单一继承 一次 指针/引用/对象访问效率相同 通过vptr和vtable访问 多态的引入,带来了设定vptr和间接访问虚函数等效率的降低
多重继承 多次 指针/引用/对象访问效率相同 通过vptr和vtable访问,通过第二或者后继base类指针访问需要调整this指针 除了单一继承效率降低的情形,调整this指针也带来了效率的降低
虚拟继承 多次 对象/指针/应用访问效率较低 通过vptr和vtable访问,访问虚基类需要调整this指针 除了单一继承效率降低的情形,调整this指针也带来了效率的降低

多态中的data member访问

考察多态中几种继承情形的data member成员访问效率的关键是:members的offset位置在编译期是否能够确定。 如果访问的成员在编译期就可以确定下offset位置,不会带来额外的负担。

理论上针对上面的继承类型,通过类对象访问,效率完全一样,因为成员在类中的位置在编译期是可以确定的。通过引用或者指针访问,除了一种情形,上面的继承类型效率也完全相同 。例外情形是:通过指针和引用访问虚拟基类的数据成员。因为虚拟基类在不同的继承层次中,其offset位置是变化的,并且无法通过指针或者引用类型确定指针指向对象的真实类型,所以编译期无法确定offset位置,只能在运行期通过类型信息确定。

实际上具体继承(非virtual继承)并不会增加空间或者存取时间上的额外负担,但是虚拟继承的“间接性”压抑了“把所有运算都移往缓存器执行”的优化能力,即使通过类对象访问编译器也会像对待指针一样(目前是,编译器都没能识别出对“继承而来的data member”的存取是通过一个非多态对象,因而不需要执行期的间接存取), 效率令人担心。但间接性并不会严重影响非优化程序的执行效率,各类型继承效率差别不大。一般来说,virtual base class最有效的运用形式:一个抽象的virtual base class,没有任何data   members。

多态中的function member访问

在C++中,nonmember/static member/nonstatic member函数都被转化为完全相同的形式(通过managling命名处理),所以它们的效率完全相同。

如果是通过引用和指针调用虚函数,效率将会降低,这是由C++多态性质决定的。而多重继承和虚拟继承中虚函数的调用比单一继承的效率更低。这个从上面表格可以清楚的看出来:this指针调(比如通过thunk技术调整)和多次初始化vptr。当然,请记住:通过对象访问虚函数和访问非虚成员函数效率是一样的。在调用虚函数而又不需要多态的情况下,可以明确地调用该函数实体:类名::函数名,压制由于虚拟机制而产生的不必要的重复调用操作。

this指针地址调整
       多重继承和虚拟继承中this指针调整使得这两种继承效率降低,实际编程时应该有所警惕。下面列出常见的需要调整this指针的情形:

1、new 派生类给第二(后继)个基类指针或通过第二(后继)base class调用派生类虚析构函数

必须调整Derived对象的地址,以使其指向Base2 subobject对象。当删除基类指向的对象时必须再一次调整,使其指向Derived对象的起始地址,然而这个调整只能在执行期完成,在编译时无法确定指针指向的对象类类型。

下次你看到这种情况不要好奇:pBase2不等于pDerived。

Derived* pDerived = new Derived;
Base2* pBase2 = pDerived; // Base2为Derived的第二个基类
pBase2 != pDerived;    // 两者不等

2、通过派生类指针调用第二或后继base class拥有的虚函数

如果想正确调用必须在编译时调整派生类指针,以指向后继base subobject调用正确的虚函数。由上面的模型图可以看到:如果通过派生类指针调用mumble函数,而mumble函数只存在于后继类的虚函数表中,故必须调整之。

3、后继base class指针调用返回derived class type的虚函数并且赋值给另一后继base class指针时

示例如下:

Base2* pb1 = new Derived;  // 调整指针指向base2 clss子对象
Base2* pb2 = pb1->clone(); // pb1被调整至Derived对象的地址,产生新的对象,再次调整对象指针指向base2基类子对象,赋值给pb2。

记住:Base class指针一定得指向一个完整的与自身类型相同的对象或者子对象地址,不满足这个条件的情形都需要this指针的调整。

详细知识请参考:《Inside The C++ Object Model》。

(0)

相关推荐

  • C++ 中继承与动态内存分配的详解

    C++ 中继承与动态内存分配的详解 继承是怎样与动态内存分配进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性.如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧.下面来看看这两种情况: 一.派生类不使用new 派生类是否需要为显示定义析构函数,复制构造函数和赋值操作符呢? 不需要! 首先,来看是否需要析构函数,如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数.实际上,派生类的默认构造

  • C++继承介绍

    然后是各个成员函数选项可以是virtual或non-virtual或pure virtual.本文仅仅作出一些关键点的验证. public继承,例如下: 复制代码 代码如下: class base {...} class derived:public base {...} 如果这样写,编译器会理解成类型为derived的对象同时也是类型为base的对象,但类型为base的对象不是类型为derived的对象.这点很重要.那么函数形参为base类型适用于derived,形参为derived不适用于b

  • C++多继承同名隐藏实例详细介绍

    如果某个派生类的部分或者全部直接基类是从另一个共同的基类派生而来,在这些俄直接基类中, 从上一级基类继承来的成员就拥有相同的名称,因此派生类中就会出现同名现象.对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须使用直接基类来进行限定. -------------------------------------------------- /* * File: main.cpp * Author: yubao * * Created on May 31, 2009, 8:54 AM */

  • 深入解析C++中类的多重继承

    C++类的多继承 在前面的例子中,派生类都只有一个基类,称为单继承.除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类. 多继承容易让代码逻辑复杂.思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java.C#.PHP 等干脆取消了多继承.想快速学习C++的读者可以不必细读. 多继承的语法也很简单,将多个基类用逗号隔开即可.例如已声明了类A.类B和类C,那么可以这样来声明派生类D: class D: public A, private B, protected C{ //类D新

  • C++继承中的访问控制实例分析

    本文较为深入的探讨了C++继承中的访问控制,对深入掌握C++面向对象程序设计是非常必要的.具体内容如下: 通常来说,我们认为一个类有两种不同的用户:普通用户 和 类的实现者.其中,普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员:实现者则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有部分.如果进一步考虑继承的话就会出现第三种用户,即派生类.派生类可以访问基类的公有(public)成员和受保护(protected)成员,但不能访问基类的私有(p

  • C++中virtual继承的深入理解

    今天专门看了一下虚继承的东西,以前都没怎么用过,具体如下:父类:  复制代码 代码如下: class   CParent { .... }; 继承类的声明比较特别: class   CChild   :   virtual   public   CParent { .... } 请问,这个"virtual"是什么作用及含义? --------------------------------------------------------------- 表示虚拟继承,和普通继承是C++的

  • 详解C++中实现继承string类的MyString类的步骤

    昨天师兄又出了道测试题,让我们实现类似于string类的没有MyString类,刚开始很头疼,可是真正在自己写代码的时候又很兴奋的发现,这个过程真的是个很宝贵的机会,让我又有机会可以很好的熟悉回顾C++的很多知识-类设计,构造析构函数,成员函数,友元函数,引用,重载,字符串操作,动态内存分布.....于是昨天花了半天时间写了300多行代码,并认真的进行了相关测试.修改和总结.因为内容有点丰富,所以想分几次写出来,条理也清楚些. 类的空间分配:类给它的每个对象都分配了独立的空间去存储它的数据成员,

  • C++多重继承与虚继承分析

    本文以实例形式较为全面的讲述了C++的多重继承与虚继承,是大家深入学习C++面向对象程序设计所必须要掌握的知识点,具体内容如下: 一.多重继承 我们知道,在单继承中,派生类的对象中包含了基类部分 和 派生类自定义部分.同样的,在多重继承(multiple inheritance)关系中,派生类的对象包含了每个基类的子对象和自定义成员的子对象.下面是一个多重继承关系图: class A{ /* */ }; class B{ /* */ }; class C : public A { /* */ }

  • C++ 多重继承和虚拟继承对象模型、效率分析

    一.多态 C++多态通过继承和动态绑定实现.继承是一种代码或者功能的传承共享,从语言的角度它是外在的.形式上的,极易理解.而动态绑定则是从语言的底层实现保证了多态的发生--在运行期根据基类指针或者引用指向的真实对象类型确定调用的虚函数功能!通过带有虚函数的单一继承我们可以清楚的理解继承的概念.对象模型的分布机制以及动态绑定的发生,即可以完全彻底地理解多态的思想.为了支持多态,语言实现必须在时间和空间上付出额外的代价(毕竟没有免费的晚餐,更何况编译器是毫无感情): 1.类实现时增加了virtual

  • 关于C++中虚拟继承的一些总结分析

    1.为什么要引入虚拟继承虚拟继承是多重继承中特有的概念.虚拟基类是为解决多重继承而出现的.如:类D继承自类B1.B2,而类B1.B2都继承自类A,因此在类D中两次出现类A中的变量和函数.为了节省内存空间,可以将B1.B2对A的继承定义为虚拟继承,而A就成了虚拟基类.实现的代码如下: class Aclass B1:public virtual A;class B2:public virtual A;class D:public B1,public B2; 虚拟继承在一般的应用中很少用到,所以也往

  • Javascript中的几种继承方式对比分析

    开篇 从'严格'意义上说,javascript并不是一门真正的面向对象语言.这种说法原因一般都是觉得javascript作为一门弱类型语言与类似java或c#之类的强型语言的继承方式有很大的区别,因而默认它就是非主流的面向对象方式,甚至竟有很多书将其描述为'非完全面向对象'语言.其实个人觉得,什么方式并不重要,重要的是是否具有面向对象的思想,说javascript不是面向对象语言的,往往都可能没有深入研究过javascript的继承方式,故特撰此文以供交流. 为何需要利用javascript实现

  • java中继承测试代码分析

    继承:可以基于已经存在的类构造一个新类.继承已经存在的类就可以复用这些类的方法和域.在此基础上,可以添加新的方法和域,从而扩充了类的功能. public class ExtendsStu { /*动物类:动物都可以动 * 1.Dog 2.Cat * 在java中,子类可以继承父类的属性和功能; * 继承关系的指定: 子类 extends 父类 * 不能被继承的资源: * 1.子类不能继承父类的构造方法,而且必须调用一个父类的构造器(因为生成子类对象的时候会初始化父类属性) * 2.私有的资源不能

  • PHP四种排序算法实现及效率分析【冒泡排序,插入排序,选择排序和快速排序】

    本文实例讲述了PHP四种排序算法实现及效率分析.分享给大家供大家参考,具体如下: PHP的四种基本排序算法为:冒泡排序.插入排序.选择排序和快速排序. 下面是我整理出来的算法代码: 1. 冒泡排序: 思路:对数组进行多轮冒泡,每一轮对数组中的元素两两比较,调整位置,冒出一个最大的数来. //简单版: function bubbleSort($arr) { $n = count($arr); for($i=1;$i<$n;$i++) { //冒泡的轮数(最多$n-1轮) for($j=0;$j<

  • Python八大常见排序算法定义、实现及时间消耗效率分析

    本文实例讲述了Python八大常见排序算法定义.实现及时间消耗效率分析.分享给大家供大家参考,具体如下: 昨晚上开始总结了一下常见的几种排序算法,由于之前我已经写了好几篇排序的算法的相关博文了现在总结一下的话可以说是很方便的,这里的目的是为了更加完整详尽的总结一下这些排序算法,为了复习基础的东西,从冒泡排序.直接插入排序.选择排序.归并排序.希尔排序.桶排序.堆排序.快速排序入手来分析和实现,在最后也给出来了简单的时间统计,重在原理.算法基础,其他的次之,这些东西的熟练掌握不算是对之后的工作或者

  • vue2.0的虚拟DOM渲染思路分析

    1.为什么需要虚拟DOM 前面我们从零开始写了一个简单的类Vue框架(文章链接),其中的模板解析和渲染是通过Compile函数来完成的,采用了文档碎片代替了直接对页面中DOM元素的操作,在完成数据的更改后通过appendChild函数将真实的DOM插入到页面. 虽然采用的是文档碎片,但是操作的还是真实的DOM. 而我们知道操作DOM的代价是昂贵的,所以vue2.0采用了虚拟DOM来代替对真实DOM的操作,最后通过某种机制来完成对真实DOM的更新,渲染视图. 所谓的虚拟DOM,其实就是 用JS来模

  • Python多重继承之菱形继承的实例详解

    继承是面向对象编程的一个重要的方式,通过继承,子类就可以扩展父类的功能.在python中一个类能继承自不止一个父类,这叫做python的多重继承(Multiple Inheritance ). 语法 class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...): pass 菱形继承 在多层继承和多继承同时使用的情况下,就会出现复杂的继承关系,多重多继承. 其中,就会出现菱形继承.如下图所示.mark 在这种结构中,在调用顺序上就出现了疑惑

  • c++ 前自增/后自增操作符效率分析

    1.前自增/后自增操作符示例 class Integer { public:     // ++i  first +1,then return new value     Integer &operator++()     {         value_ += 1;         return *this;     }       // i++  first save old value,then +1,last return old value     Integer operator++

随机推荐