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

一、菱形继承

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

演示案例

class A
{
public:
    A(int a) :m_a(a) {}
    int getMa() { return m_a; }
private:
    int m_a;
};

class B :public A
{
public:
    B(int a, int b) :A(a), m_b(b) {}
private:
    int m_b;
};

class C :public A
{
public:
    C(int a, int c) :A(a), m_c(c) {}
private:
    int m_c;
};

class D :public B, public C
{
public:
    D(int a, int b, int c, int d) :B(a, b), C(a, c), m_d(d) {}
    void func()
    {
        /*错误,访问不明确
        std::cout << getMa();*/

        //正确,通过B访问getMa()
        std::cout << B::getMa();
    }
private:
    int m_d;
};

二、虚继承

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

演示案例

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

class ZooAnimal {}; //虚基类

class Bear :public virtual ZooAnimal {};    //虚继承
class Raccoon :public virtual ZooAnimal {}; //虚继承

//Panda只保存一份ZooAnimal的定义
class Panda :public Bear, public Raccoon, public Endangered {};

三、虚继承中的类型转换

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

class Bear :public virtual ZooAnimal {};
class Raccoon :public virtual ZooAnimal {};

class Panda :public Bear, public Raccoon, public Endangered {};

void dance(const Bear&);
void rummage(const Raccoon&);
ostream& operator<<(ostream&, const ZooAnimal&);

int main()
{
    Panda ying_yang;

    dance(ying_yang);  //正确,把一个Panda对象当成Bear传递
    rummage(ying_yang);//正确,把一个Panda对象当成Raccoon 传递
    cout << ying_yang; //正确,把一个Panda对象当成ZooAnimal传递

    return 0;
}

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

  • 规则如下:
    • 虚基类的成员没有被任何派生类隐藏,那么该成员可以直接访问,并且不会产生二义性
    • 如果虚基类的成员只被一条派生路径隐藏,则我们仍然可以直接访问这个被隐藏的版本
    • 如果虚基类的成员多多个派生路径隐藏,则会产生二义性
  • 例如,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,从而造成错误)。因此我们需要在间接派生类中为虚基类进行构造,从而避免了重复构造的二义性

演示案例

//普通继承
class A {
public:
    A(int a);
};

class B :public A {
public:
    B(int a):A(10) {}
};

class C :public B {
public:
    C() :B(10) {} //可以不为A进行构造,因为A的构造已经交给B了
};
//虚继承
class A {
public:
    A(int a);
};

class B :virtual public A {
public:
    B(int a):A(10) {}
};

class C :virtual public A {
public:
    C(int a) :A(10) {}
};

class D :public B,public C {
public:
    //D() :B(10), C(20) {} 错误的,必须显式为A进行构造
    D() :A(5), B(10), C(20) {} //正确
};

构造函数的执行顺序

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

class Character {};
class BookCharacter :public Character {};

class ZooAnimal {};
class Bear :public virtual ZooAnimal {};

class ToyAnimal {};

class ReddyBear :public BookCharacter, public Bear, public virtual ToyAnimal {};
  • 析构函数:析构函数的执行顺序与构造函数执行顺序相反
发布了1481 篇原创文章 · 获赞 1026 · 访问量 38万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104442356
今日推荐