虚拟继承是C++语言中一个非常重要但是又比较生僻的存在,它的定义非常简单,但是对于理解C++的继承机制却是非常有用的。笔者最近学习过程中发现对C++的虚拟继承不是很明朗,故在这里对虚继承做个小结。

首先说下遇到的问题吧。代码如下(代码来自于何海涛《程序员面试精选100题第32题)。意图是要设计一个不能被继承的类,类似java中的final。但是又可以生成栈对象,可以像一般的C++类一样使用。

[cpp] view plain copy  
  1. #include "stdafx.h"
  2. #include <iostream>
  3. using namespace std;
  4. template <class T> class MakeFinal
  5. {
  6. friend T;
  7. private:
  8. MakeFinal()
  9. {
  10. cout<<"in MakeFinal"<<endl;
  11. }
  12. ~MakeFinal(){}
  13. };
  14. class FinalClass: virtual public MakeFinal<FinalClass>
  15. {
  16. public:
  17. FinalClass()
  18. {
  19. cout<<"in FinalClass"<<endl;
  20. }
  21. ~FinalClass(){}
  22. };
  23. class Try: public FinalClass
  24. {
  25. public:
  26. Try()
  27. {
  28. cout<<"in Try"<<endl;
  29. }
  30. };

这样的确使得FinalClass不能被继承了,原因在于类FinalClass是从类MakeFinal<Final>虚继承过来的,在调用Try的构造函数的时候,会直接跳过FinalClass而直接调用MakeFinal<FinalClass>的构造函数。而Try不是MakeFinal<Final>的友元,所以这里就会出现编译错误。但是如果把虚继承改成一般的继承,这里就没什么问题了。笔者对这里的调用顺序不是很明朗,为了对虚继承有彻底的了解,故做个小结。将从下面几个方向进行总结:1、为何要有虚继承;2、虚继承对于类的对象布局的影响;3、虚基类对构造函数的影响;

1、为什么需要虚继承

由于C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如像下面的情况

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected Base
  15. {
  16. public:
  17. DerivedA()
  18. {
  19. cout<<"in DerivedA"<<endl;
  20. }
  21. };
  22. class DerivedB: protected Base
  23. {
  24. public:
  25. DerivedB()
  26. {
  27. cout<<"in DerivedB"<<endl;
  28. }
  29. };
  30. class MyClass:DerivedA,DerivedB
  31. {
  32. public:
  33. MyClass()
  34. {
  35. cout<<"in MyClass"<<value<<endl;
  36. }
  37. };

编译时的错误如下


这中情况下会造成在MyClass中访问value时出现路径不明确的编译错误,要访问数据,就需要显示地加以限定。变成DerivedA::value或者DerivedB::value,以消除歧义性。并且,通常情况下,像Base这样的公共基类不应该表示为两个分离的对象,而要解决这种问题就可以用虚基类加以处理。如果使用虚继承,编译便正常了,类的结构示意图便如下。

虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示,正是如上图所示。

2、虚继承对类的对象布局的影响

要理解多重继承情况中重复基类时为什么会出现访问路径不明确的编译错误,需要了解继承中类对象在内存中的布局。在C++继承中,子类会继承父类的成员变量,因此在子类对象在内存中会包括来自父类的成员变量。实例代码如下,输出结果表明了每个对象在内存中所占的大小。

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. //cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected  Base
  15. {
  16. protected:
  17. int valueA;
  18. public:
  19. DerivedA()
  20. {
  21. //cout<<"in DerivedA"<<endl;
  22. }
  23. };
  24. class DerivedB: protected  Base
  25. {
  26. protected:
  27. int valueB;
  28. public:
  29. DerivedB()
  30. {
  31. //cout<<"in DerivedB"<<endl;
  32. }
  33. };
  34. class MyClass:DerivedA
  35. {
  36. private:
  37. int my_value;
  38. public:
  39. MyClass()
  40. {
  41. //cout<<"in MyClass"<<value<<endl;
  42. }
  43. };
  44. int main()
  45. {
  46. Base base_obj;
  47. DerivedA derA_obj;
  48. MyClass my_obj;
  49. cout<<"size of Base object "<<sizeof(base_obj)<<endl;
  50. cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
  51. cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;
  52. }

输出结果如下

从类的定义结合这里的输出便不难明白,在子类对象中是包含了父类数据的,即在C++继承中,一个子类的object所表现出来的东西,是其自己的members加上其基类的member的总和。示意图如下(这里只讨论非静态变量)

在单继承的时候,访问相关的数据成员时,只需要使用名字即可。但是,在多重继承时,情况会变得复杂。因为重复基类中,在子类中变量名是相同的。这时,如果直接使用名字去访问,便会出现歧义性。看下面的代码以及对应的输出

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. //cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected  Base
  15. {
  16. protected:
  17. int valueA;
  18. public:
  19. DerivedA()
  20. {
  21. //cout<<"in DerivedA"<<endl;
  22. }
  23. };
  24. class DerivedB: protected  Base
  25. {
  26. protected:
  27. int valueB;
  28. public:
  29. DerivedB()
  30. {
  31. //cout<<"in DerivedB"<<endl;
  32. }
  33. };
  34. class MyClass:DerivedA,DerivedB
  35. {
  36. private:
  37. int my_value;
  38. public:
  39. MyClass()
  40. {
  41. //cout<<"in MyClass"<<value<<endl;
  42. }
  43. };
  44. int main()
  45. {
  46. Base base_obj;
  47. DerivedA derA_obj;
  48. MyClass my_obj;
  49. cout<<"size of Base object "<<sizeof(base_obj)<<endl;
  50. cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
  51. cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;
  52. }

输出如下

代码的变化之处在于MyClass同时继承了DerivedA和DerivedB。而my_obj在内存中的大小变成了20,比之前大了8.正好是增加了继承至DerivedB中的数据部分的大小。上面情况中,my_obj在内存中的布局示意图如下

从图中可以看到,来自Base基类的数据成员value重复出现了两次。这也正是为什么在MyClass中直接访问value时会出现访问不明确的问题了。

那么使用虚继承后,对象的数据在内存中的布局又是什么样子呢?按照预测,既然在my_obj中只有一份来自Base的value,那么大小是否就是16呢?

代码及输出如下

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. //cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected  virtual Base
  15. {
  16. protected:
  17. int valueA;
  18. public:
  19. DerivedA()
  20. {
  21. //cout<<"in DerivedA"<<endl;
  22. }
  23. };
  24. class DerivedB: protected virtual Base
  25. {
  26. protected:
  27. int valueB;
  28. public:
  29. DerivedB()
  30. {
  31. //cout<<"in DerivedB"<<endl;
  32. }
  33. };
  34. class MyClass:DerivedA,DerivedB
  35. {
  36. private:
  37. int my_value;
  38. public:
  39. MyClass()
  40. {
  41. //cout<<"in MyClass"<<value<<endl;
  42. }
  43. };
  44. int main()
  45. {
  46. Base base_obj;
  47. DerivedA derA_obj;
  48. DerivedB derB_obj;
  49. MyClass my_obj;
  50. cout<<"size of Base object "<<sizeof(base_obj)<<endl;
  51. cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
  52. cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
  53. cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;
  54. };

输出结果如下

可以看到,DerivedA和DerivedB对象的大小变成了12,而MyClass对象的大小则变成了24.似乎大大超出了我们的预料。这其实是由于编译器在其中插入了一些东西用来寻找这个共享的基类数据所用而造成的。(来自《深度探索C++对象模型》第3章 侯捷译)这样理解,Class如果内含一个或多个虚基类子对象,那么将被分割为两部分:一个不变部分和一个共享部分。不变局部中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以直接存取。至于共享局部,所表现的就是虚基类子对象。根据编译其的不同,会有不同的方式去得到这部分的数据,但总体来说都是需要有一个指向这部分共享数据的指针。

示意图如下

当然实际编译器使用的技术比这个要复杂,这里就不做详细讨论了。感兴趣的朋友可以参见《深入探索C++对象模型》

3、虚继承对构造函数的影响

对于构造函数的影响,借助于下面的原则可以理解(来自《深入理解C++对象模型》)

构造函数的调用可能内带大量的隐藏码,因为编译器会对构造函数进行扩充,一般而言编译器所作的扩充操作大约如下:

1、记录在成员初始化列表中的数据成员的初始化操作会被放到构造函数本身中,按照数据成员声明的顺序

2、如果有一个数据成员没有出现在初始化列表中,但是它有一个默认构造函数,那么这个默认构造函数会被调用

3、在那之前,如果有虚函数表,会调整虚函数表指针

4、在那之前,会对上一层基类的构造函数进行调用

5、在那之前,所有虚基类的构造函数必须被调用,按照声明的继承顺序从左往右,从最深到最浅的顺序

从上面的规则可以看出,对于虚基类的构造函数的调用是放在最前面的,并且需要子类对于虚基类的构造函数拥有访问权限

从下面的示例代码可以看出

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected  Base
  15. {
  16. protected:
  17. int valueA;
  18. public:
  19. DerivedA()
  20. {
  21. cout<<"in DerivedA"<<endl;
  22. }
  23. };
  24. class DerivedB
  25. {
  26. protected:
  27. int valueB;
  28. public:
  29. DerivedB()
  30. {
  31. cout<<"in DerivedB"<<endl;
  32. }
  33. };
  34. class TestClass
  35. {
  36. public:
  37. TestClass()
  38. {
  39. cout<<"in TestClass"<<endl;
  40. }
  41. };
  42. class MyClass:DerivedA,virtual DerivedB
  43. {
  44. private:
  45. int my_value;
  46. TestClass testData;
  47. public:
  48. MyClass()
  49. {
  50. //cout<<"in MyClass"<<value<<endl;
  51. }
  52. };
  53. int main()
  54. {
  55. /*
  56. Base base_obj;
  57. DerivedA derA_obj;
  58. DerivedB derB_obj;
  59. MyClass my_obj;
  60. cout<<"size of Base object "<<sizeof(base_obj)<<endl;
  61. cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
  62. cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
  63. cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;*/
  64. MyClass my_obj;
  65. }

代码运行后的效果如下所示

虽然在声明继承顺序的时候DerivedA的顺序是在DerivedB的前面的,但是由于DerivedB是虚拟继承,所以对DerivedB的调用会在最前。但是如果将DerivedA继承也改成虚继承,那么调用顺序就会发生变化。并且具体DerivedA的构造函数的调用与DerivedB的构造函数的调用顺序还与是MyClass虚继承DerivedA还是DerivedA虚继承Base有关。还是用代码来说明

MyClass虚继承DerivedA的情况,由于DerivedA声明在DerivedB前面,并且都是虚继承,所以先调用DerivedA的构造函数。但DerivedA的父类是Base,所以具体的构造函数的调用顺序是Base、DerivedA、DerivedB、TestClass。这种情况代码如下

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected  Base
  15. {
  16. protected:
  17. int valueA;
  18. public:
  19. DerivedA()
  20. {
  21. cout<<"in DerivedA"<<endl;
  22. }
  23. };
  24. class DerivedB
  25. {
  26. protected:
  27. int valueB;
  28. public:
  29. DerivedB()
  30. {
  31. cout<<"in DerivedB"<<endl;
  32. }
  33. };
  34. class TestClass
  35. {
  36. public:
  37. TestClass()
  38. {
  39. cout<<"in TestClass"<<endl;
  40. }
  41. };
  42. class MyClass:virtual DerivedA,virtual DerivedB
  43. {
  44. private:
  45. int my_value;
  46. TestClass testData;
  47. public:
  48. MyClass()
  49. {
  50. //cout<<"in MyClass"<<value<<endl;
  51. }
  52. };
  53. int main()
  54. {
  55. /*
  56. Base base_obj;
  57. DerivedA derA_obj;
  58. DerivedB derB_obj;
  59. MyClass my_obj;
  60. cout<<"size of Base object "<<sizeof(base_obj)<<endl;
  61. cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
  62. cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
  63. cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;*/
  64. MyClass my_obj;
  65. }

输出如下

但如果将虚继承放在由DerivedA虚继承Base,而MyClass非虚继承DerivedA。那么按照从左往右,从深到浅的顺序,调用顺序应该是Base、DerivedB、DerivedA、TestClass

,代码示例如下

[cpp] view plain copy  
  1. #include<iostream>
  2. using std::cout;
  3. using std::endl;
  4. class Base
  5. {
  6. protected:
  7. int value;
  8. public:
  9. Base()
  10. {
  11. cout<<"in Base"<<endl;
  12. }
  13. };
  14. class DerivedA:protected virtual  Base
  15. {
  16. protected:
  17. int valueA;
  18. public:
  19. DerivedA()
  20. {
  21. cout<<"in DerivedA"<<endl;
  22. }
  23. };
  24. class DerivedB
  25. {
  26. protected:
  27. int valueB;
  28. public:
  29. DerivedB()
  30. {
  31. cout<<"in DerivedB"<<endl;
  32. }
  33. };
  34. class TestClass
  35. {
  36. public:
  37. TestClass()
  38. {
  39. cout<<"in TestClass"<<endl;
  40. }
  41. };
  42. class MyClass: DerivedA,virtual DerivedB
  43. {
  44. private:
  45. int my_value;
  46. TestClass testData;
  47. public:
  48. MyClass()
  49. {
  50. //cout<<"in MyClass"<<value<<endl;
  51. }
  52. };
  53. int main()
  54. {
  55. /*
  56. Base base_obj;
  57. DerivedA derA_obj;
  58. DerivedB derB_obj;
  59. MyClass my_obj;
  60. cout<<"size of Base object "<<sizeof(base_obj)<<endl;
  61. cout<<"size of DerivedA object "<<sizeof(derA_obj)<<endl;
  62. cout<<"size of DerivedB object "<<sizeof(derB_obj)<<endl;
  63. cout<<"size of MyClass object "<<sizeof(my_obj)<<endl;*/
  64. MyClass my_obj;
  65. }

输出如下

这里还有一个问题是,左右顺序和深浅顺序该如何抉择呢?答案是先左右,再深浅。比如将上面的代码改为class MyClas: virutal DerivedB, DerivedA{...}

那么最后的调用顺序就是DerivedB,Base, DerivedA,TestClass。读者可以自己验证。

C++虚继承(九) --- 构造函数调用顺序的实用之处相关推荐

  1. 继承构造函数调用顺序_C ++中带有继承的构造函数调用的顺序

    继承构造函数调用顺序 Base class constructors are always called in the derived class constructors. Whenever you ...

  2. 菱形继承中构造函数调用问题

    菱形继承中构造函数调用问题 在某一个虚基类的任何一个派生类的构造函数中,都要将该虚基类的构造函数显示列出来. 包含虚基类的派生类对象的构造函数的调用顺序如下: 虚基类的构造函数在非虚基类之前调用. 若 ...

  3. C++ 基类,子对象,派生类构造函数调用顺序

    #include <iostream> using namespace std;class A {public:A( ) {cout << "A Constructo ...

  4. Java构造函数调用顺序问题

    今天对Java的构造函数调用顺序进行研究,使用的是与C++类似的方法,即不对源码进行研究,而是直接通过打印代码对构造函数的调用顺序进行研究. 代码如下,使用的是Java核心技术中的代码,对其进行了改造 ...

  5. C++虚继承(七) --- 虚继承对基类构造函数调用顺序的影响

    继承作为面向对象编程的一种基本特征,其使用频率非常高.而继承包含了虚拟继承和普通继承,在可见性上分为public.protected.private.可见性继承比较简单,而虚拟继承对学习c++的难度较 ...

  6. C/C++---中多继承构造函数调用顺序

    class B1 {public:B1(int i) {cout<<"consB1"<<i<<endl;} };//定义基类B1 class B ...

  7. java构造函数调用其他程序的顺序_java初始化构造函数调用顺序

    看我大师归来: 1. Base b = new Sub(); 2. Base b = 直接忽略,从 new Sub();开始 3. 类加载器加载 Base,Sub 类到jvm; 4. 为Base,Su ...

  8. 虚继承c语言例子,C/C++ 多继承{虚基类,虚继承,构造顺序,析构顺序}

    C/C++:一个基类继承和多个基类继承的区别 1.对多个基类继承会出现类之间嵌套时出现的同名问题,如果同名变量或者函数出现不在同一层次,则底层派生隐藏外层比如继承基类的同名变量和函数,不会出现二义性, ...

  9. ☆ C++ 继承与派生(包括虚继承)

    在友元类中我们知道,一旦在一个类中声明了友元类,那么友元类便拥有了访问该类的所有权限,可以在自己的类中对声明自己的类进行一系列操作. 友元类主要目的是为了拓展友元类的功能,但是友元类的权限未免太多了, ...

最新文章

  1. 丘成桐:中国人可以做世界一流学者
  2. 驳Linux不娱乐 堪比Win平台中十款播放器
  3. leetcode -python 三数之和原创
  4. 关于 MySQL5.7.log 版本导出 SQL 语句再导入 8.0.13 版本出现 Incorrect datetime value: ‘0000-00-00 00:00:00‘ 错误的解决办法
  5. windows常用进程
  6. 拒绝做焦虑贩卖者的韭菜
  7. idea怎么直接拉去git_如何将GitHub上面的项目拉取到IDEA中
  8. Java开发需要达到什么样的水平才称得上架构师?
  9. Ubuntu 源码方式安装Subversion
  10. python单例模式和装饰器
  11. codevs1958 刺激
  12. usermod 添加用户多个附属组
  13. 海量数据存储的解决方案(分布式数据库)
  14. android 版本 6.0升级包,EMUI 6.0系统刷机包
  15. Lorenz系统、简单的Rossler系统和Chua电路系统的混沌吸引子——MATLAB实现
  16. 在职复习考研计算机408,考研初试复习经验分享(计算机408)
  17. APP的文件数据直传腾讯云COS实践
  18. 使用h5的方式来实现钟表
  19. dubbo的简单搭建
  20. 情感分析的分类,情感分析模型有哪些,情感分析的应用场景,情感分析的发展趋势

热门文章

  1. React个Vue的对比
  2. 函数的返回值-改造求和函数
  3. Python程序执行原理
  4. SpringBoot_配置-properties配置文件编码问题
  5. 设计模式之_Iterator_06
  6. SpringBoot 配置Tomcat运行
  7. 数据库建立索引、数据表创建规则、备用字段 / 保留字段 / 预留字段
  8. autoflowchart软件使用步骤_编程怎么入门,7个步骤带你飞, 网友:上车!
  9. CSS中盒模型的理解
  10. Linux 初始化脚本 (centos6 centos7 通用)