多重继承与虚继承

多重继承

多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。

class Bear : public ZooAnimal{/*...*/};
class Panda : public Bear,public Endangered{/*...*/};

每个基类包含一个可选的访问说明符,如果访问符被忽略掉,则关键字 class 对应的默认访问符是 private,关键字 struct 对应的是 public。

多重继承的派生列表也只能包含已经被定义过的类,而且这些类不能是 final 的。

对于派生类能够继承的基类个数,C++ 没有明确的规定,但是在某个给定的派生列表中,同一个基类只能出现一次。

多重继承的派生类从每个基类中继承状态

派生类的对象包含每个基类的子对象:

Panda 对象中含有:

  • Bear 的部分,Bear 中含有 ZooAnimal 部分。
  • 含有 Endangered 的部分。
  • 还有 Panda 中声明的非静态数据成员。

派生类构造函数初始化所有基类

构造一个派生类的对象将同时构造并初始化它的所有基类子对象 。

显式的初始化所有基类:

Panda ::Panda(std::string name,bool onExhibit)
    :Bear(name,onExhibit,"Panda"),
    :Endangered(Endangered::critical){}

隐式地使用 Bear 的默认构造函数初始化 Bear 子对象

Panda::Panda()
    :Endangered(Endangered::critical) {}

基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。

Panda 对象的初始化过程:

  • 首先初始化 Bear 继承体系的最终基类: ZooAnimal。
  • 初始化 Panda 的第一个直接基类 Bear。
  • 初始化 Panda 的第二个直接基类 Endangered。
  • 最后初始化 Panda。

继承的构造函数与多重继承

在 C++ 11 中,允许派生类从它的一个或几个基类中继承构造函数,但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同),则程序将产生错误:

struct Base1 {
    Base1() = default;
    Base1(const std::string&);
    Base1(std::shared_ptr<int>);
};

struct Base2 {
    Base2() = default;
    Base2(const std::string&);
    Base2(int);
};

//@ error:D1 试图从两个基类中都继承 D1::D1(const string&)
struct D1 : public Base1, public Base2 {
    using Base1::Base1;     //@ 从 Base1 继承构造函数
    using Base2::Base2;     //@ 从 Base2 继承构造函数
};

如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本:

struct D2 : public Base1,public Base2
{
    using Base1::Base1;
    using Base2::Base2;
    //@ D2 必须自定义一个接受 stding 的构造函数
    D2(const string& s):Base1(s),Base2(s){}
    D2() = default; //@ 一旦D2定义了它自己的构造函数,则必须出现
};

析构函数与多重继承

派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的。合成的析构函数体为空。

析构函数的调用顺序正好与构造函数相反,在本例中,析构函数的调用顺序是:~Panda,~Endangered,~Bear和 ~ZooAnimal。

多重继承的派生类的拷贝与移动操作

与只有一个基类的继承一样,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝,移动或赋值成员时,才会自动对其基类部分执行这些操作。在合成 的控制成员中,每个基类分别使用自己的对应成员隐式完成构造,赋值和销毁等工作。

例如,假设 Panda 使用了合成版本的成员 ling_ling 的初始化过程:

Panda ying_yang("ying_yang");
Panda ling_ling = ying_yang;    //@ 使用拷贝构造函数

上面的过程将调用 Bear 的拷贝构造函数,后者又在执行自己的拷贝任务之前先调用 ZooAnimal 的拷贝构造函数。

一旦 ling_ling 的 Bear部分构造完成,接着就会调用 Endangered 的拷贝构造函数来创建对象相应的部分。

最后执行 Panda 的拷贝构造函数。

合成的移动构造函数的工作机理与之类似。

合成的拷贝赋值运算符的行为与拷贝构造函数很相似。它首先赋值 Bear 部分,并且通过 Bear 赋值给 ZooAnimal 部分。然后赋值 Endangered 部分,最后是 Panda 部分。移动赋值运算符的工作机理与之类似。

类型转换与多个基类

当只有一个基类的情况下,派生类的指针或引用能自动转换成一个可访问基类的指针或引用。

多个基类的情况与之类似,可以令某个可访问基类的指针或引用直接指向一个派生类对象:

void print(const Bear&);
void highlight(const Endangered&);
ostream& operator<<(ostream&,const ZooAnimal&);

Panda ying_yang("ying_yang");
printf(ying_yang);          //@ 把一个 Panda 对象传递给 Bear 的引用
highlight(ying_yang);       //@ 把一个 Panda 对象传递给 Endangered 的引用
cout << ying_yang << endl;      //@ 把一个 Panda 对象传递给 ZooAnimal 的引用

编译器不会在派生类向基类转换中进行比较,在它看来转换到任意一种基类都是一样的好:

void print(const Bear&);
void print(const Endangered&);

则通过对象不带前缀限定符的 print 将会产生编译错误:

Panda ying_yang("ying_yang");
print(ying_yang);   //@ 二义性错误

基于指针类型或引用类型的查找

与只有一个基类的继承一样,对象,指针和引用的静态类型决定了我们能够使用哪些成员。

假设继承体系中定义的虚函数:

函数 含有自定义版本的类
print ZooAnimal::ZooAnimal Bear::Bear Endangered::Endangered Panda::Panda
highlight Endangered::Endangered Panda::Panda
toes Bear::Bear Panda::Panda
cuddle Panda::Panda
析构函数 ZooAnimal::ZooAnimal Endangered::Endangered
Bear *pb = new Panda("ying_yang");
pb->print();    //@ ok, Panda::print
pb->cuddle();   //@ error,cuddle 不属于 Bear
pb->highlight();    //@ error,cuddle 不属于 Bear
delete pb;          //@ ok,Panda::~Panda

当通过 Endangered 的指针或引用访问一个 Panda 时,Panda 接口中特有的以及属于 Bear 的部分将是不可见的:

Endangered  *pe = new Panda("ying_yang");
pe->print();    //@ ok, Panda::print
pe->toes();     //@ error,toes 不属于 Endangered
pe->cuddle();   //@ error,cuddle 不属于 Endangered
pe->highlight();    //@ ok, Panda::highlight
delete pe;      //@ ok,Panda::~Panda

多重继承下来的类作用域

在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。查找过程沿着继承体系自底向上进行,直到找到所需要的名字。派生类的名字将隐藏基类的同名成员。

在多重继承的情况下,相同的查找过程在所有的直接基类中同时进行,如果名字在多个基类中都被找到,则对该名字的使用具有二义性。

对于一个派生类来说,从它的几个基类中分别继承名字相同的成员是完全合法的,只不过在使用这个名字时必须明确指出它的版本。

例如,如果 ZooAnimal 和 Endangered 都定义了名为 max_weight 的成员。并且 Panda 没有定义该成员,则下面的调用是错误的:

double d = ying_yang.max_weight();  

正确的调用方式:

double d = ZooAnimal::max_weight(); 
double d = Endangered::max_weight();    

编译器将会先查找名字,然后进行类型检查,当编译器在两个作用域中同时发现了相同的名字,将会产生二义性错误:

  • 即使派生类继承的两个函数参数列表不同也可能发生错误。
  • 即使 max_weight 在一个基类中是私有的,而在另一个类中是公有的或者受保护的,同样可能发生错误。
  • 假如 max_weight 定义在 Bear 而非 ZooAnimal中,仍然是错误的。

要想避免二义性问题,最好的办法就是在派生类中为该函数顶一个新版本:

double Panda::max_weight() const
{
    return std::max(ZooAnimal::max_weight(),Endangered::max_weight());
}

虚继承

尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类:

  • 派生类可以通过它的两个直接基类分别继承同一个间接基类。
  • 派生类也可以直接继承某个基类,再通过另一个基类再一次间接继承该类。

默认情况下,派生类中含有继承链上每个类对应的子部分。如果基类在派生过程中出现了多次,则派生类中包含该类的多个子对象。

例如 iostream 直接继承自 istream 和 ostream,istream 和 ostream 都继承自 base_ios,base_ios 是一个抽象基类,保存了 流的缓冲内容并管理流的条件状态。

一个 iostream 对象肯定希望在同一个缓冲区中进行读写操作,也会要求条件状态同时能反映输入和输出操作的情况。假如 iostream 对象中包含了 base_ios 的两份拷贝,则上述共享行为则无法实现。

在 C++ 中 通过虚继承的机制解决上述问题,虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

假设 Panda 的继承体系如下:

虚继承有一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。例如:定义 Panda 时才出现了对虚派生的需求,但是如果 Bear 和 Raccoon 不是从 ZooAnimal 虚派生得到的,那么 Panda 的设计者就显得不太幸运。

使用虚基类

指定虚基类的方式是在派生列表这种添加关键字 virtual,public 和 virtual 的顺序随意:

class Raccon : public virtual ZoomAnimal {/*...*/};
class Bear : public virtual ZoomAnimal {/*...*/};

通过上面的代码将 ZoomAnimal 定义为 Raccon 和 Bear 的虚基类。

virtual 说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例,至于什么样的类能够作为虚基类并没有特殊规定。

如果某个类指定了虚基类,则该类的派生仍按常规方式进行:

class Panda : public Bear,public Raccoon,public Endangered{/*...*/};

Panda 中将只有一份 ZooAnimal 基类。

支持向基类的常规类型转换

不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。例如,下面这些从 Panda 向基类的类型转换都是合法的:

void dance(const Bear&);
void rummage(const Raccoon&);
ostream& operator<<(ostream&,const ZooAnimal&);
Panda ying_yang;
dance(ying_yang);       //@ ok,把一个 Panda 对象当成 Bear 传递
rummage(ying_yang);     //@ ok,把一个 Panda 对象当成 Raccoon 传递
cout << ying_yang;      //@ ok,把一个 Panda 对象当成 ZooAnimal 传递

虚基类成员的可见性

在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。

  • 如果虚基类的成员只被一条派生路径覆盖,则仍然可以直接访问这个被覆盖的成员。
  • 如果成员被多余一个基类覆盖,则一般情况下派生类必须为该成员定义一个新的版本。

假如,类 B 定义了一个名为 x 的成员,D1 和 D2 都是从 B 继承得到的,D 继承了 D1 和 D2,则在D 的作用域中,x 通过 D 的两个基类都是可见的。如果通过D的对象使用 x,有三种可能性:

  • 如果在 D1 和 D2 中都没有 x 的定义,则 x 将被解析为 B 的成员,此时不存在二义性,一个 D 的对象只含有 x 的一个实例。
  • 如果 x 是 B 的成员,同时是 D1 和 D2 中某一个的成员,则同样没有二义性,派生类的 x 比共享虚基类 B 的 x 优先级更高。
  • 如果在 D1 和 D2 中都有 x 的定义,则直接访问 x 将产生二义性问题。

与非虚的多重继承体系一样,解决这种二义性问题最好的方法是在派生类中为成员自定义新的实例。

构造函数与虚继承

在虚派生中,虚基类是由最低层的派生类初始化的。

例如:当创建 Panda 对象时,由 Panda 的构造函数独自控制 ZooAnimal 的初始化过程。假设以普通规则处理初始化任务,在此例中,虚基类将会在多条继承路径被重复初始化。例如 Raccoon 和 Bear 都会试图初始化 Panda 对象的 ZooAnimal 部分。

当然,继承体系中的每个类都可能在某个时刻称为"最低层的派生类"。只要能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。例如床架一个 Bear 或者 Raccoon 对象时,它已经位于派生的最低层次,因此 Bear 或 Raccoon 的构造函数将直接初始化其 ZooAnimal 基类部分:

Bear::Bear(std::string name, bool onExhibit) :
    ZooAnimal(name,onExhibit,"Bear") {}
Raccoon::Raccoon(std::string name, bool onExhibit) :
    ZooAnimal(name, onExhibit, "Raccoon") {}

而当创建一个Panda 对象时,Panda 位于派生的最底层并由它负责初始化共享的 ZooAnimal 基类部分。即使 ZooAnimal 不是Panda 的直接基类,Panda 的构造函数也可以直接初始化 ZooAnimal:

Panda::Panda(std::string name,bool onExhibit)
    :ZooAnimal(name, onExhibit,"Panda"),
    Bear(name,onExhibit),
    Raccoon(name,onExhibit),
    Endangered(Endangered::critical),
    sleeping_flag(){}

虚继承的对象的构造方式

虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。

含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。

例如,当创建一个 Panda 对象时:

  • 首先使用 Panda 的构造函数初始值列表中提供的初始值构造虚基类 ZooAnimal 部分。
  • 接下来构造 Bear 部分。
  • 然后构造Raccoon 部分。
  • 然后构造第三个直接基类 Endangered。
  • 最后构造 Panda 部分。

如果 Panda 没有显式地初始化 ZooAnimal 基类,则 ZooAnimal 的默认构造函数被调用,ZooAnimal 如果没有默认构造函数,则代码将发生错误。

构造函数与析构函数的次序

一个类可以有多个虚基类,此时,这些虚的子对象按照从左向右依次构造。

例如: TeddyBear 派生体系中有两个虚基类:ToyAnimal 是直接虚基类,ZooAnimal 是 Bear 的虚基类:

class Character{/*...*/};
class BookCharacter : public Character {/*...*/ };
class ToyAnimal{/*...*/};
class TeddyBear:public BookCharacter,
                public Bear,public virtual ToyAnimal
                {};

编译器按照直接基类的声明顺序对其依次进行检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按照声明的顺序逐一构造其它非虚基类。

创建一个 TeddyBear 对象,需要按照如下顺序调用构造函数:

  • ZooAnimal(); //@ Bear 的虚基类
  • ToyAnimal(); //@ 直接虚基类
  • Character(); //@ 第一个非虚基类的间接基类
  • BookCharacter(); //@ 第一个直接非虚基类
  • Bear(); //@ 第二个直接非虚基类
  • TeddyBear(); //@ 最低层的派生类

合成的拷贝和移动构造函数按照完全相同的顺序执行,合成的赋值运算符中的成员也按照该顺序赋值,和往常一样,对象的销毁顺序与构造构造顺序正好相反。

猜你喜欢

转载自www.cnblogs.com/xiaojianliu/p/12459513.html