小言_互联网的博客

C++(继承):19---虚基类与虚继承(virtual)

362人阅读  评论(0)

一、菱形继承

  • 在介绍虚继承之前介绍一下菱形继承
  • 概念:A作为基类,B和C都继承与A。最后一个类D又继承于B和C,这样形式的继承称为菱形继承
  • 菱形继承的缺点:
    • 数据冗余:在D中会保存两份A的内容
    • 访问不明确(二义性):因为D不知道是以B为中介去访问A还是以C为中介去访问A,因此在访问某些成员的时候会发生二义性
  • 缺点的解决:
    • 数据冗余:通过下面“虚继承”技术来解决(见下)
    • 访问不明确(二义性):通过作用域访问符::来明确调用。虚继承也可以解决这个问题

演示案例


   
  1. class A
  2. {
  3. public:
  4. A( int a) :m_a(a) {}
  5. int getMa() { return m_a; }
  6. private:
  7. int m_a;
  8. };
  9. class B : public A
  10. {
  11. public:
  12. B( int a, int b) :A(a), m_b(b) {}
  13. private:
  14. int m_b;
  15. };
  16. class C : public A
  17. {
  18. public:
  19. C( int a, int c) :A(a), m_c(c) {}
  20. private:
  21. int m_c;
  22. };
  23. class D : public B, public C
  24. {
  25. public:
  26. D( int a, int b, int c, int d) :B(a, b), C(a, c), m_d(d) {}
  27. void func()
  28. {
  29. /*错误,访问不明确
  30. std::cout << getMa();*/
  31. //正确,通过B访问getMa()
  32. std:: cout << B::getMa();
  33. }
  34. private:
  35. int m_d;
  36. };

二、虚继承

  • 虚继承的作用:为了保证公共继承对象在创建时只保存一分实例
  • 虚继承解决了菱形继承的两个问题:
    • 数据冗余:顶级基类在整个体系中只保存了一份实例
    • 访问不明确(二义性):可以不通过作用域访问符::来调用(原理就是因为顶级基类在整个体系中只保存了一份实例)
  • 共享的基类对象成为“虚基类”
  • 说明:虚继承不会影响派生类本身,只是对虚基类进行的说明
  • 通过在继承列表中使用virtual关键字来说明,virtual与继承说明符(public、protected、private)的位置可以互换

演示案例

  • 下面的ZooAnimal是一个虚基类,Bear和Raccoon分别虚继承于ZooAnimal


   
  1. class ZooAnimal {}; //虚基类
  2. class Bear : public virtual ZooAnimal {}; //虚继承
  3. class Raccoon : public virtual ZooAnimal {}; //虚继承
  4. //Panda只保存一份ZooAnimal的定义
  5. class Panda : public Bear, public Raccoon, public Endangered {};

三、虚继承中的类型转换

  • 虚继承中也可以将派生类抓换为基类,用基类的指针/引用指向于派生类
  • 例如:

  
  1. class ZooAnimal {};
  2. class Bear : public virtual ZooAnimal {};
  3. class Raccoon : public virtual ZooAnimal {};
  4. class Panda : public Bear, public Raccoon, public Endangered {};
  5. void dance(const Bear&);
  6. void rummage(const Raccoon&);
  7. ostream& operator<<(ostream&, const ZooAnimal&);
  8. int main()
  9. {
  10. Panda ying_yang;
  11. dance(ying_yang); //正确,把一个Panda对象当成Bear传递
  12. rummage(ying_yang); //正确,把一个Panda对象当成Raccoon 传递
  13. cout << ying_yang; //正确,把一个Panda对象当成ZooAnimal传递
  14. return 0;
  15. }

四、虚基类成员的可见性与隐藏

  • 规则如下:
    • 虚基类的成员没有被任何派生类隐藏,那么该成员可以直接访问,并且不会产生二义性
    • 如果虚基类的成员只被一条派生路径隐藏,则我们仍然可以直接访问这个被隐藏的版本
    • 如果虚基类的成员多多个派生路径隐藏,则会产生二义性
  • 例如,D1和D2虚继承与B,D继承于D1和D2,并且B有一个x成员:
    • 如果D1和D2都没有x的定义:此时对x的访问不会产生二义性,因为只含有x的一个实例
    • 如果D1中有x的定义而D2没有:同样没有二义性,派生类的x比虚基类B的x优先级更高(或者D1中没有x的定义而D2有x的定义)
    • 如果D1和D2都有x的定义:对x的访问会产生二义性

  • 解决二义性最好的办法就是在派生类为成员自定义新的实例

五、虚继承的构造函数

  • 虚继承中的构造函数与普通继承的构造函数不一样:
    • 普通继承:派生类可以不为间接基类(基类的基类)进行构造函数的调用
    • 虚继承:不论派生类属于哪一层,派生类都需要对虚基类进行构造
  • 原因:假设以下间接派生类没有为虚基类进行构造,那么当间接派生类进行构造时,会对虚基类进行重复的构造函数的调用(例如下面的演示案例D如果不显式构造A,那么当构造B和C的时候,B和C都会构造一次A,从而造成错误)。因此我们需要在间接派生类中为虚基类进行构造,从而避免了重复构造的二义性

演示案例


   
  1. //普通继承
  2. class A {
  3. public:
  4. A( int a);
  5. };
  6. class B : public A {
  7. public:
  8. B( int a):A( 10) {}
  9. };
  10. class C : public B {
  11. public:
  12. C() :B( 10) {} //可以不为A进行构造,因为A的构造已经交给B了
  13. };

   
  1. //虚继承
  2. class A {
  3. public:
  4. A( int a);
  5. };
  6. class B : virtual public A {
  7. public:
  8. B( int a):A( 10) {}
  9. };
  10. class C : virtual public A {
  11. public:
  12. C( int a) :A( 10) {}
  13. };
  14. class D : public B, public C {
  15. public:
  16. //D() :B(10), C(20) {} 错误的,必须显式为A进行构造
  17. D() :A( 5), B( 10), C( 20) {} //正确
  18. };

构造函数的执行顺序

  • 规则:虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关
  • 例如,在上面的演示案例中,构造顺序为:A-->B-->C-->D
  • 下面再演示一个有多个虚基类的例子,其构造函数执行熟悉怒为:
    • ZooAnimal
    • ToyAnimal
    • Character
    • BookCharacter
    • Bear
    • TeddyBear


   
  1. class Character {};
  2. class BookCharacter : public Character {};
  3. class ZooAnimal {};
  4. class Bear : public virtual ZooAnimal {};
  5. class ToyAnimal {};
  6. class ReddyBear : public BookCharacter, public Bear, public virtual ToyAnimal {};
  • 析构函数:析构函数的执行顺序与构造函数执行顺序相反

转载:https://blog.csdn.net/qq_41453285/article/details/104442356
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场