C++ 多重继承和虚拟继承对象模型、效率分析_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》。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索多重继承
虚拟继承
多重继承c 对象模型、超效率dea模型介绍、虚拟继承、面向对象继承、c 虚拟继承,以便于您获取更多的相关知识。

时间: 2024-09-20 00:16:50

C++ 多重继承和虚拟继承对象模型、效率分析_C 语言的相关文章

关于C++中虚拟继承的一些总结分析_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; 虚拟继承在一般的应用中很少用到,所以也往

C++类的多重继承与虚拟继承

在过去的学习中,我们始终接触的单个类的继承,但是在现实生活中,一些新事物往往会拥有两个或者两个以上事物的属性,为了解决这个问题,C++引入了多重继承的概念,C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承. 举个例子,交通工具类可以派生出汽车和船连个子类,但拥有汽车和船共同特性水陆两用汽车就必须继承来自汽车类与船类的共同属性. 由此我们不难想出如下的图例与代码: 当一个派生类要使用多重继承的时候,必须在派生类名和冒号之后列出所有基类的类名,并用逗好分隔. //程序作者:管宁//站

C++多重继承与虚继承分析_C 语言

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

C++中virtual继承的深入理解_C 语言

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

C语言的递归思想实例分析_C 语言

本文实例分析C语言的递归思想,分享给大家供大家参考之用.具体方法如下: 通俗点来说,递归就是自己调用自己. 递归的难点一是理解递归的执行调用过程,二是设置一个合理的递归结束条件. 下面来看一段摘自书中的简单程序: #include <STDIO.H> long fact(int n); long rfact(int n); int main(void) { int num; printf("This program calculates factorials.\n"); p

C++实现不能被继承的类实例分析_C 语言

本文实例展示了C++实现不能被继承的类的方法,对于C++初学者而言有一定的学习借鉴价值.具体实现方法如下: 方法一: #include <iostream> using namespace std; class A { public: static A* getInstance(); static void deleteInstance(A* pA); private: A() { cout << "construct A\n";} ~A() { cout &l

C++继承中的访问控制实例分析_C 语言

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

基于C++执行内存memcpy效率测试的分析_C 语言

在进行memcpy操作时,虽然是内存操作,但是仍然是耗一点点CPU的,今天测试了一下单线程中执行memcpy的效率,这个结果对于配置TCP epoll中的work thread 数量有指导意义.如下基于8K的内存快执行memcpy, 1个线程大约1S能够拷贝500M,如果服务器带宽或网卡到上限是1G,那么网络io的work thread 开2个即可,考虑到消息的解析损耗,3个线程足以抗住硬件的最高负载. 在我到测试机器上到测试结果是: Intel(R) Xeon(R) CPU          

关于C++使用指针 堆和栈的区别分析_C 语言

数据在内存的存放有以下几种形式 1.栈区--由编译器自动分配并且释放,该区域一般存放函数的参数值,局部变量的值等, 2.堆区--一般由程序员分配释放,如果程序员不释放,程序结束的时候才会被操作系统回收,3.寄存器区--用来保存栈顶指针和指令指针4.全局去--也是静态区,全局变量和静态变量都是存储在一起的,初始化的全局变量和静态变量都存储在一块,为初始化的全局变量和静态变量在相邻的另一个区域,程序结束后由系统释放.5.文字常量区--常量字符串就是放在这里的,程序结束后由系统释放,6.程序代码区--