天天看点

C++:53---菱形继承、虚继承

一、菱形继承

  • 在介绍虚继承之前介绍一下菱形继承
  • 概念: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
  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 {};

三、虚继承中的类型转换

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

菱形继承中的类型转换

  • 菱形继承中会发生错误,不能将派生类转换为基类
  • 原理是差不多的,就是因为派生类中拥有多份基类的实体,因此不能转换,会产生二义性
class A {};
class B: publi A {};
class C: publi A {};
class D: public B, public C {};




int main()
{
D d;
A* pa = &d; //错误
return 0;
}      

虚继承中的类型转换

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 {};      

析构函数

  • 析构函数的执行顺序与构造函数执行顺序相反