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

首先,虚拟继承与普通继承的区别有:

假设derived 继承自base类,那么derived与base是一种“is a”的关系,即derived类是base类,而反之错误;

假设derived 虚继承自base类,那么derivd与base是一种“has a”的关系,即derived类有一个指向base类的vptr。(貌似有些牵强!某些编译器确实如此,关于虚继承与普通继承的差异见:c++ 虚继承与继承的差异 )

因此虚继承可以认为不是一种继承关系,而可以认为是一种组合的关系。正是因为这样的区别,下面我们针对虚拟继承来具体分析。虚拟继承中遇到最广泛的是菱形结构。下面从菱形虚继承结构说起吧:

[cpp] view plaincopy
  1. class stream
  2. {
  3. public:
  4. stream(){cout<<"stream::stream()!"<<endl;}
  5. };
  6. class iistream:virtual stream
  7. {
  8. public:
  9. iistream(){cout<<"istream::istream()!"<<endl;}
  10. };
  11. class oostream:virtual stream
  12. {
  13. public:
  14. oostream(){cout<<"ostream::ostream()!"<<endl;}
  15. };
  16. class iiostream:public iistream,public oostream
  17. {
  18. public:
  19. iiostream(){cout<<"iiostream::iiostream()!"<<endl;}
  20. };
  21. int main(int argc, const char * argv[])
  22. {
  23. iiostream oo;

程序运行的输出结果为:

stream::stream()!

istream::istream()!

ostream::ostream()!

iiostream::iiostream()!   

输出这样的结果是毫无悬念的!本来虚拟继承的目的就是当多重继承出现重复的基类时,其只保存一份基类。减少内存开销。其继承结构为:

stream

/               \

istream   ostream

\                 /

iiostream

这样子的菱形结构,使公共基类只产生一个拷贝。

从基类 stream 派生新类时,使用 virtual 将类stream说明为虚基类,这时派生类istream、ostream包含一个指向虚基类的vptr,而不会产生实际的stream空间。所以最终iiostream也含有一个指向虚基类的vptr,调用stream中的成员方法时,通过vptr去调用,不会产生二义性。

而现在我们换种方式使用虚继承:

[cpp] view plaincopy
  1. class stream
  2. {
  3. public:
  4. stream(){cout<<"stream::stream()!"<<endl;}
  5. };
  6. class iistream:public stream
  7. {
  8. public:
  9. iistream(){cout<<"istream::istream()!"<<endl;}
  10. };
  11. class oostream:public stream
  12. {
  13. public:
  14. oostream(){cout<<"ostream::ostream()!"<<endl;}
  15. };
  16. class iiostream:virtual iistream,virtual oostream
  17. {
  18. public:
  19. iiostream(){cout<<"iiostream::iiostream()!"<<endl;}
  20. };
  21. int main(int argc, const char * argv[])
  22. {
  23. iiostream oo;

其输出结果为:

stream::stream()!

istream::istream()!

stream::stream()!

ostream::ostream()!

iiostream::iiostream()!

从结果可以看到,其构造过程中重复出现基类stream的构造过程。这样就完全没有达到虚拟继承的目的。其继承结构为:

stream              stream

\                     /

istream    ostream

\                   /

iiostream

从继承结构可以看出,如果iiostream对象调用基类stream中的成员方法,会导致方法的二义性。因为iiostream含有指向其虚继承基类 istream,ostreamvptr。而 istream,ostream包含了stream的空间,所以导致iiostream不知道导致是调用那个stream的方法要解决改问题,可以指定vptr,即在调用成员方法是需要加上作用域,例如

[cpp] view plaincopy
  1. class stream
  2. {
  3. void f(){cout<<"here!"<<endl;}
  4. }
  5. main()
  6. {
  7. iiostream ii;
  8. ii.f();
  9. }

编译器提示调用f方法错误。而采用

[cpp] view plaincopy
  1. ii.istream::f();

编译通过,并且会调用istream类vptr指向的f()方法。 前面说了这么多,在实际的应用中虚拟继承的胡乱使用,更是会导致继承顺序以及基类构造顺序的混乱。如下面的代码:

[cpp] view plaincopy
  1. class B1
  2. {
  3. public:
  4. B1(){cout<<"B1::B1()!<"<<endl;}
  5. void f() {cout<<"i'm here!"<<endl;}
  6. };
  7. class V1: public B1
  8. {
  9. public:
  10. V1(){cout<<"V1::V1()!<"<<endl;}
  11. };
  12. class D1: virtual public V1
  13. {
  14. public:
  15. D1(){cout<<"D1::D1()!<"<<endl;}
  16. };
  17. class B2
  18. {
  19. public:
  20. B2(){cout<<"B2::B2()!<"<<endl;}
  21. };
  22. class B3
  23. {
  24. public:
  25. B3(){cout<<"B3::B3()!<"<<endl;}
  26. };
  27. class V2:public B1, public B2
  28. {
  29. public:
  30. V2(){cout<<"V2::V2()!<"<<endl;}
  31. };
  32. class D2:virtual public V2, public B3
  33. {
  34. public:
  35. D2(){cout<<"D2::D2()!<"<<endl;}
  36. };
  37. class M1
  38. {
  39. public:
  40. M1(){cout<<"M1::M1()!<"<<endl;}
  41. };
  42. class M2
  43. {
  44. public:
  45. M2(){cout<<"M2::M2()!<"<<endl;}
  46. };
  47. class X:public D1, public D2
  48. {
  49. M1 m1;
  50. M2 m2;
  51. };
  52. int main(int argc, const char * argv[])
  53. {
  54. X x;

上面的代码是来自《Exceptional C++ Style》中关于继承顺序的一段代码。可以看到,上面的代码继承关系非常复杂,而且层次不是特别的清楚。而虚继承的加入更是让继承结构更加无序。不管怎么样,我们还是可以根据c++的标准来分析上面代码的构造顺序。c++对于创建一个类类型的初始化顺序是这样子的:

1.最上层派生类的构造函数负责调用虚基类子对象的构造函数。所有虚基类子对象会按照深度优先、从左到右的顺序进行初始化;

2.直接基类子对象按照它们在类定义中声明的顺序被一一构造起来;

3.非静态成员子对象按照它们在类定义体中的声明的顺序被一一构造起来;

4.最上层派生类的构造函数体被执行。

根据上面的规则,可以看出,最先构造的是虚继承基类的构造函数,并且是按照深度优先,从左往右构造。因此,我们需要将继承结构划分层次。显然上面的代码可以认为是4层继承结构。其中最顶层的是B1,B2类。第二层是V1,V2,V3。第三层是D1,D2.最底层是X。而D1虚继承V1,D2虚继承V2,且D1和D2在同一层。所以V1最先构造,其次是V2.在V2构造顺序中,B1先于B2.虚基类构造完成后,接着是直接基类子对象构造,其顺序为D1,D2.最后为成员子对象的构造,顺序为声明的顺序。构造完毕后,开始按照构造顺序执行构造函数体了。所以其最终的输出结果为:

B1::B1()!<

V1::V1()!<

B1::B1()!<

B2::B2()!<

V2::V2()!<

D1::D1()!<

B3::B3()!<

D2::D2()!<

M1::M1()!<

M2::M2()!<

从结果也可以看出其构造顺序完全符合上面的标准。而在结果中,可以看到B1重复构造。还是因为没有按照要求使用virtual继承导致的结果。要想只构造B1一次,可以将virtual全部改在B1上,如下面的代码:

[cpp] view plaincopy
  1. class B1
  2. {
  3. public:
  4. B1(){cout<<"B1::B1()!<"<<endl;}
  5. void f() {cout<<"i'm here!"<<endl;}
  6. };
  7. class V1: virtual public B1   //public修改为virtual
  8. {
  9. public:
  10. V1(){cout<<"V1::V1()!<"<<endl;}
  11. };
  12. class D1:  public V1
  13. {
  14. public:
  15. D1(){cout<<"D1::D1()!<"<<endl;}
  16. };
  17. class B2
  18. {
  19. public:
  20. B2(){cout<<"B2::B2()!<"<<endl;}
  21. };
  22. class B3
  23. {
  24. public:
  25. B3(){cout<<"B3::B3()!<"<<endl;}
  26. };
  27. class V2:virtual public B1, public B2 //public B1修改为virtual public B1
  28. {
  29. public:
  30. V2(){cout<<"V2::V2()!<"<<endl;}
  31. };
  32. class D2: public V2, public B3
  33. {
  34. public:
  35. D2(){cout<<"D2::D2()!<"<<endl;}
  36. };
  37. class M1
  38. {
  39. public:
  40. M1(){cout<<"M1::M1()!<"<<endl;}
  41. };
  42. class M2
  43. {
  44. public:
  45. M2(){cout<<"M2::M2()!<"<<endl;}
  46. };
  47. class X:public D1, public D2
  48. {
  49. M1 m1;
  50. M2 m2;
  51. };

根据上面的代码,其输出结果为:

B1::B1()!<

V1::V1()!<

D1::D1()!<

B2::B2()!<

V2::V2()!<

B3::B3()!<

D2::D2()!<

M1::M1()!<

M2::M2()!<

由于虚继承导致其构造顺序发生比较大的变化。不管怎么,分析的规则还是一样。

上面分析了这么多,我们知道了虚继承有一定的好处,但是虚继承会增大占用的空间。这是因为每一次虚继承会产生一个vptr指针。空间因素在编程过程中,我们很少考虑,而构造顺序却需要小心,因此使用未构造对象的危害是相当大的。因此,我们需要小心的使用继承,更要确保在使用继承的时候保证构造顺序不会出错。下面我再着重强调一下基类的构造顺序规则:

1.最上层派生类的构造函数负责调用虚基类子对象的构造函数。所有虚基类子对象会按照深度优先、从左到右的顺序进行初始化;

2.直接基类子对象按照它们在类定义中声明的顺序被一一构造起来;

3.非静态成员子对象按照它们在类定义体中的声明的顺序被一一构造起来;

4.最上层派生类的构造函数体被执行。

C++虚继承(七) --- 虚继承对基类构造函数调用顺序的影响相关推荐

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

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

  2. C++派生类与基类构造函数调用次序

    本文用来测试C++基类和派生类构造函数,析构函数,和拷贝构造函数的调用次序. 运行环境:SUSE Linux Enterprise Server 11 SP2  (x86_64) #include & ...

  3. C++ day24 继承(四)抽象基类,纯虚函数,protected

    文章目录 关键字protected(带来方便同时带来危险,最好不用) 抽象基类和纯虚函数(is-a关系用公有继承实现有时候也不太合适) 用圆和椭圆的笨拙派生为例,挑拨is-a和公有继承的搭档关系 替代 ...

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

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

  5. C++之继承探究(十):抽象基类与纯虚函数

    前文:C++之继承探究(九):多态的代价 抽象基类与纯虚函数   ♠\spadesuit♠ 纯虚函数:虚函数只有声明,函数体=0,就是一个纯虚函数,纯虚函数没有函数体,不需要实现.在子类里实现纯虚函数 ...

  6. C++虚继承(九) --- 构造函数调用顺序的实用之处

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

  7. 读书笔记_Effective_C++_条款七:为多态基类声明virtual析构函数

    严格来说,多态分为编译时多态和运行时多态,编译时多态是函数的重载,而运行时多态则是迟绑定技术,即根据基类指针或引用的实际指向来决定采取何种行动,一般来说,多态特指运行时多态.下面我们来举有关C++多态 ...

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

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

  9. C++虚继承(八) --- 虚继承与继承的差异

    前面一篇文章,说明了在C++ 虚继承对基类构造函数调用顺序的影响.经过仔细推敲,发现没有彻底说清楚虚继承与普通继承之间的关系.所以用下面的文字再说明一下. 首先,重复一下虚拟继承与普通继承的区别有: ...

最新文章

  1. 数据库中表id自增重置为1
  2. 乐视手机权限开启方法
  3. HDFS2.X架构及工作原理
  4. Memcache缓存系统原理
  5. 解決memcache 有時無法連接的問題
  6. 前端学习(1879)vue之电商管理系统电商系统之通过axios拦截器添加token认证
  7. 【算法分析与设计】找到最重的球
  8. asp.net捕获全局未处理异常的几种方法
  9. idea下使用Maven找不到类
  10. alwayson高可用组_了解AlwaysOn可用性组上的备份-第2部分
  11. java商品名称_Java统计商品信息
  12. 喜欢就争取,得到就珍惜,错过就忘记—dbGet(二)
  13. 优酷路由宝刷梅林_【荒野无灯Padavan固件】优酷路由宝L1内存卡扩展SWAP缓存+v2瑞设置详解...
  14. access设置 dolby_win10系统设置和安装新款杜比音效的方法
  15. 批量下载GLDAS数据
  16. OGRE: Ogre第一个程序
  17. RTP协议学习大总结从原理到代码
  18. python sql语句换行_python一行sql太长折成多行并且有多个参数的方法
  19. 正则表达式中[A-z]和[a-zA-Z]的区别
  20. Hydra 6.4.xx,Hydra程序框架

热门文章

  1. 字符串-字符串的查找和替换
  2. 适配器模式源码解析(jdk+spring+springjpa+springmvc)
  3. postgresql主从备份_PostgreSQL主从流复制与手动主备切换架构
  4. Activemq-In-action(二)
  5. sed模式空间(pattern space)和保持空间(hold space)
  6. python组成不重复的三位数是多少_超星Python 练习实例1-组成多少个互不相同且无重复的三位数字...
  7. des算法明文IP置换C语言编程,求助攻:C语言DES算法的实现程序有问题
  8. yii2 关掉php notice,yii2关闭错误提示
  9. python 消息队列、异步分布式
  10. Java学习路线详解