原文声明:http://blog.sina.com.cn/s/blog_93b45b0f01011pkz.html

虚继承和虚基类的定义是非常的简单的,同时也是非常容易判断一个继承是否是虚继承的,虽然这两个概念的定义是非常的简单明确的,但是在C++语言中虚继承作为一个比较生僻的但是又是绝对必要的组成部份而存在着,并且其行为和模型均表现出和一般的继承体系之间的巨大的差异(包括访问性能上的差异),现在我们就来彻底的从语言、模型、性能和应用等多个方面对虚继承和虚基类进行研究。

首先还是先给出虚继承和虚基类的定义。

    虚继承:在继承定义中包含了virtual关键字的继承关系;

    虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class Base1 : public virtual Base0 {}; 其中Base0称之为Base1的虚基类,而不是说Base0就是个虚基类,因为Base0还可以不不是虚继承体系中的基类。

    有了上面的定义后,就可以开始虚继承和虚基类的本质研究了,下面按照语法、语义、模型、性能和应用五个方面进行全面的描述。

1. 语法

       语法有语言的本身的定义所决定,总体上来说非常的简单,如下:

class Base1 : public virtual Base0 {};

其中可以采用public、protected、private三种不同的继承关键字进行修饰,只要确保包含virtual就可以了,这样一来就形成了虚继承体系,同时Base0就成为了Base1的虚基类了。其实并没有那么的简单,如果出现虚继承体系的进一步继承会出现什么样的状况呢?

如下所示:

class Base0
{
public:
Base0(int i) : m_val( i ) {}
int m_val;
};
class Base1 : public virtual Base0
{
public:
Base1 (int i) : Base0( i ) {}
}; class Base2 : public virtual Base0
{
public:
Base2(int i) : Base0( i ) {}
}; class Base3 : public Base1, public Base2
{
public:
Base3(int i) : Base0( i ), Base1 ( i ), Base2 ( i ) {}
}; class Base4: public Base3
{
public:
Base4 (int i) : Base0( i ), Base3( i ) {}
};

注意上面代码中的Base3和Base4两个类的构造函数初始化列表中的内容。可以发现其中均包含了虚基类Base0的初始化工作,如果没有这个初始化语句就会导致编译时错误,为什么会这样呢?一般情况下不是只要在Base1和Base2中包含初始化就可以了么?要解释该问题必须要明白虚继承的语义特征,所以参看下面语义部分的解释。
2. 语义

       从语义上来讲什么是虚继承和虚基类呢?上面仅仅是从如何在C++语言中书写合法的虚继承类定义而已。

       首先来了解一下virtual这个关键字在C++中的公共含义,在C++语言中仅仅有两个地方可以使用virtual这个关键字,一个就是类成员虚函数和这里所讨论的虚继承。不要看这两种应用场合好像没什么关系,其实他们在背景语义上具有virtual这个词所代表的共同的含义,所以才会在这两种场合使用相同的关键字。

       那么virtual这个词的含义是什么呢?virtual在《美国传统词典[双解]》中是这样定义的:

           adj.(形容词)

           1. Existing or resulting in essence or effect though not in actual fact, form, or name:

              实质上的,实际上的:虽然没有实际的事实、形式或名义,但在实际上或效果上存在或产生的;

           2. Existing in the mind, especially as a product of the imagination. Used in literary criticism of text.

              虚的,内心的:在头脑中存在的,尤指意想的产物。用于文学批评中。

       我们采用第一个定义,也就是说被virtual所修饰的事物或现象在本质上是存在的,但是没有直观的形式表现,无法直接描述或定义,需要通过其他的间接方式或手段才能够体现出其实际上的效果。

       那么在C++中就是采用了这个词意,不可以在语言模型中直接调用或体现的,但是确实是存在可以被间接的方式进行调用或体现的。比如:虚函数必须要通过一种间接的运行时(而不是编译时)机制才能够激活(调用)的函数,而虚继承也是必须在运行时才能够进行定位访问的一种体制。存在,但间接。其中关键就在于存在、间接和共享这三种特征。

       对于虚函数而言,这三个特征是很好理解的,间接性表明了他必须在运行时根据实际的对象来完成函数寻址(C++中利用类指针*与引用&来完成多样性的重载),共享性表象在基类会共享被子类重载后的虚函数,其实指向相同的函数入口。

       对于虚继承而言,这三个特征如何理解呢?存在即表示虚继承体系和虚基类确实存在,间接性表明了在访问虚基类的成员时同样也必须通过某种间接机制来完成(下面模型中会讲到),共享性表象在虚基类会在虚继承体系中被共享,而不会出现多份拷贝。

       那现在可以解释语法小节中留下来的那个问题了,“为什么一旦出现了虚基类,就必须在每一个继承类中都必须包含虚基类的初始化语句”。

       由上面的分析可以知道,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的),这样一来既然是共享的那么每一个子类都不会独占,但是总还是必须要有一个类来完成基类的初始化过程(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中(也是很自然的)选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),而在最下层继承子类中实际执行初始化过程。所以上面在每一个继承类中都要书写初始化语句,但是在创建对象时,而仅仅会在创建对象用的类构造函数中实际的执行初始化语句,其他的初始化语句都会被压制不调用。
 3. 模型

       为了实现上面所说的三种语义含义,在考虑对象的实现模型(也就是内存模型)时就很自然了。在C++中对象实际上就是一个连续的地址空间的语义代表,我们来分析虚继承下的内存模型。

       3.1. 存在

           也就是说在对象内存中必须要包含虚基类的完整子对象,以便能够完成通过地址完成对象的标识。那么至于虚基类的子对象会存放在对象的那个位置(头、中间、尾部)则由各个编译器选择,没有差别。(在VC8中无论虚基类被声明在什么位置,虚基类的子对象都会被放置在对象内存的尾部)

       3.2. 间接

           间接性表明了在直接虚基承子类中一定包含了某种指针(偏移或表格)来完成通过子类访问虚基类子对象(或成员)的间接手段(因为虚基类子对象是共享的,没有确定关系),至于采用何种手段由编译器选择。(在VC8中在子类中放置了一个虚基类指针vbc,该指针指向虚函数表中的一个slot,该slot中存放着虚基类子对象的偏移量的负值,实际上就是个以补码表示的int类型的值,在计算虚基类子对象首地址时,需要将该偏移量取绝对值相加,这个主要是为了和虚表中只能存放虚函数地址这一要求相区别,因为地址是原码表示的无符号int类型的值)

       3.3. 共享

           共享表明了在对象的内存空间中仅仅能够包含一份虚基类的子对象,并且通过某种间接的机制来完成共享的引用关系。在介绍完整个内容后会附上测试代码,体现这些内容。
    4. 性能

       由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

       4.1. 时间

           在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。

           (在VC8中通过打开汇编输出,可以查看*.cod文件中的内容,在访问虚基类对象成员时会形成三条mov间接寻址语句,而在访问一般继承类对象时仅仅只有一条mov常量直接寻址语句)

       4.2. 空间

           由于共享所以不同在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。
    5. 应用

       谈了那么多语言特性和内容,那么在什么情况下需要使用虚继承,而一般应该如何使用呢?

       这个问题其实很难有答案,一般情况下如果你确性出现多继承没有必要,必须要共享基类子对象的时候可以考虑采用虚继承关系(C++标准ios体系就是这样的)。由于每 一个继承类都必须包含初始化语句而又仅仅只在最底层子类中调用,这样可能就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的(因为自己的初始化语句被压制了),所以一般建议不要在虚基类中包含任何数据成员(不要有状态),只可以作为接口类来提供。

最后声明:c++中的虚基类对应java中的接口,Java中的接口则没有任何实现代码,而且接口里面的属性默认都是public static, 所有方法都是public 的。所以java用起来确实又很多方便之处。

C++ 虚基类的定义、功能、规定的更多相关文章

  1. 虚基类——(1)定义人员类Person: 公有成员:姓名(Name); 保护成员:性别(Gender),年龄(Age); 构造函数和析构函数

    题目描述: (1)定义人员类Person: 公有成员:姓名(Name): 保护成员:性别(Gender),年龄(Age): 构造函数和析构函数 (2) 从人员类Person派生学生记录类Student ...

  2. 不可或缺 Windows Native (22) - C++: 多重继承, 虚基类

    [源码下载] 不可或缺 Windows Native (22) - C++: 多重继承, 虚基类 作者:webabcd 介绍不可或缺 Windows Native 之 C++ 多重继承 虚基类 示例1 ...

  3. C++:虚基类

    4.4.3 虚基类1.没什么要引入虚基类 如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最底层的派生类中会保留这个间接的共同基类数据成员的多分同名成员.在访问这些同名的成员时,必须在 ...

  4. C++虚基类详解(转)

    我们知道,如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员.在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避 ...

  5. C++学习之路—继承与派生(三):多重继承与虚基类

    (根据<C++程序设计>(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明) 多重继承是指一个派生类有两个或多个基类.例如,有 ...

  6. c++ 虚基类应用

    多重继承存在二义性,为了消除二义性在访问相同名称的属性时需要加上类名,加以区分.虽然这样可以解决二义性,但是相同的属性出现在多个基类中,为了解决数据冗余,c++引入了虚基类. 虚基类定义:class ...

  7. 基类中定义的虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型及参数的先后顺序,都必须与基类中的原型完全相同 but------> 可以返回派生类对象的引用或指针

      您查询的关键词是:c++primer习题15.25 以下是该网页在北京时间 2016年07月15日 02:57:08 的快照: 如果打开速度慢,可以尝试快速版:如果想更新或删除快照,可以投诉快照. ...

  8. RTTI、虚函数和虚基类的实现方式、开销分析及使用指导(虚函数的开销很小,就2次操作而已)

    白杨 http://baiy.cn “在正确的场合使用恰当的特性” 对称职的C++程序员来说是一个基本标准.想要做到这点,首先要了解语言中每个特性的实现方式及其开销.本文主要讨论相对于传统 C 而言, ...

  9. C#虚基类继承与接口的区别

    类:定义新的数据类型以及这些新的数据类型进行相互操作的方法 定义方式: class Cat { } class Cat:object { } C#中所有的类都是默认由object类派生来的,显示指定或 ...

随机推荐

  1. CF1066F Yet another 2D Walking

    DP 由图可以知道优先级相同的点都在一个"7"字形中 所以在走当前的优先级的点时最好从右下的点走到左上的点,或从从左上的点走到右下的点 那记dp[i][0]表示在走完第i个优先级时 ...

  2. Core WebApi项目快速入门(三):踩坑笔记

    目前做公司一个项目,遇到了一些坑.跟大家分享,避免再次采坑. 1. 服务端发布应用报错 在windows server上发布程序报错.系统缺少更新包. https://support.microsof ...

  3. 【JVM第一篇--类加载机制】类加载过程

    写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.什么是类加载过程 (1).概述 我们编写的类(.java文件)会被编译器(如ja ...

  4. CMake编译OpenCV4.0时opencv_ffmpeg.dll等下载失败的解决思路总结

    一.Configure会报一些红色的警告信息,比如: 1 CMake Warning at cmake/OpenCVDownload.cmake:193 (message): 2 FFMPEG: Do ...

  5. Golang 接口型函数和http.Handler接口

    一.接口型函数 参考Golang必备技巧:接口型函数 1.原始接口实现 type Handler interface { Do(k, v interface{}) } func Each(m map[ ...

  6. ceph各个版本之间参数变化分析

    前言 本篇主要是分析ceph的版本之间参数的变化,参数变化意味着功能的变化,通过参数来分析增加,删除,修改了哪些功能,以及版本之间的变化,本篇主要通过导出参数,然后通过脚本去比对不同的版本的参数变化 ...

  7. pytorch框架对RTX 2080Ti RTX 3090的支持与性能测试

    时间点:202011-18 一.背景 2020年9月nvidia发布了30系列的显卡.比起20系列网上的评价是:性能翻倍,价格减半. 最近正好本人手上有RTX 2080Ti 和 RTX 3090,所以 ...

  8. python爬虫 selenium 抓取 今日头条(ajax异步加载)

    from selenium import webdriver from lxml import etree from pyquery import PyQuery as pq import time ...

  9. 写的太细了!Spring MVC拦截器的应用,建议收藏再看!

    Spring MVC拦截器 拦截器是Spring MVC中强大的控件,它可以在进入处理器之前做一些操作,或者在处理器完成后进行操作,甚至是在渲染视图后进行操作. 拦截器概述 对于任何优秀的MVC框架, ...

  10. 总是说spring难学?来看完这些spring的注解及其解释,真香!

    前言 用过spring的人都知道,spring简单的通过注解就可以完成很多事情,但这些东西是如何实现的呢以及如何应用到我们自己的代码中?接下来,让我们一起开启注解的旅程. 1. @Controller ...